Skip to content

Commit

Permalink
Merge pull request #63 from phase2/develop
Browse files Browse the repository at this point in the history
Version 1.3.1
  • Loading branch information
febbraro authored Aug 15, 2017
2 parents a55d874 + b4ae58a commit 05c5997
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 89 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 1.3.1

- Don't start NFS if not on Darwin
- Auto update generator (project create) and dashboard images
- Added flag to disable autoupdate of generator (project create) image
- Added doctor check for Docker env var configuration
- Added doctor check for `/data` and `/Users` usage
- Added configurable timeouts for sync start
- Added detection when sync start has finished initializing

## 1.3.0

- `Commands()` function now returns an array of cli.Command structs instead of a single struct
Expand Down
16 changes: 14 additions & 2 deletions cli/commands/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (cmd *Dashboard) Commands() []cli.Command {
}
}

func (cmd *Dashboard) Run(c *cli.Context) error {
func (cmd *Dashboard) Run(ctx *cli.Context) error {
if cmd.machine.IsRunning() {
cmd.out.Info.Println("Launching Dashboard")
cmd.LaunchDashboard(cmd.machine)
Expand All @@ -41,8 +41,20 @@ func (cmd *Dashboard) LaunchDashboard(machine Machine) {
exec.Command("docker", "stop", "outrigger-dashboard").Run()
exec.Command("docker", "rm", "outrigger-dashboard").Run()

dockerApiVersion, _ := util.GetDockerServerApiVersion(cmd.machine.Name)
image := "outrigger/dashboard:latest"

// The check for whether the image is older than 30 days is not currently used.
_, seconds, err := util.ImageOlderThan(image, 86400*30)
if err == nil {
cmd.out.Verbose.Printf("Local copy of the image '%s' was originally published %0.2f days ago.", image, seconds/86400)
}

cmd.out.Verbose.Printf("Attempting to update %s", image)
if err := util.StreamCommand(exec.Command("docker", "pull", image)); err != nil {
cmd.out.Verbose.Println("Failed to update dashboard image. Will use local cache if available.")
}

dockerApiVersion, _ := util.GetDockerServerApiVersion(cmd.machine.Name)
args := []string{
"run",
"-d",
Expand Down
59 changes: 56 additions & 3 deletions cli/commands/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package commands

import (
"fmt"
"os"
"os/exec"
"runtime"
"strings"

"github.com/hashicorp/go-version"
"github.com/phase2/rig/cli/util"
"github.com/urfave/cli"
"strconv"
)

type Doctor struct {
Expand All @@ -26,8 +29,28 @@ func (cmd *Doctor) Commands() []cli.Command {
}

func (cmd *Doctor) Run(c *cli.Context) error {
// 1. Ensure the configured docker-machine matches the set environment.
if cmd.machine.Exists() {
if _, isset := os.LookupEnv("DOCKER_MACHINE_NAME"); isset == false {
cmd.out.Error.Fatalf("Docker configuration is not set. Please run 'eval \"$(rig config)\"'.")
} else if cmd.machine.Name != os.Getenv("DOCKER_MACHINE_NAME") {
cmd.out.Error.Fatalf("Your environment configuration specifies a different machine. Please re-run as 'rig --name=\"%s\" doctor'.", cmd.machine.Name)
} else {
cmd.out.Info.Printf("Docker Machine (%s) name matches your environment configuration.", cmd.machine.Name)
}
if output, err := exec.Command("docker-machine", "url", cmd.machine.Name).Output(); err == nil {
hostUrl := strings.TrimSpace(string(output))
if hostUrl != os.Getenv("DOCKER_HOST") {
cmd.out.Error.Fatalf("Docker Host configuration should be '%s' but got '%s'. Please re-run 'eval \"$(rig config)\"'.", os.Getenv("DOCKER_HOST"), hostUrl)
} else {
cmd.out.Info.Printf("Docker Machine (%s) URL (%s) matches your environment configuration.", cmd.machine.Name, hostUrl)
}
}
} else {
cmd.out.Error.Fatalf("No machine named '%s' exists. Did you run 'rig start --name=\"%s\"'?", cmd.machine.Name, cmd.machine.Name)
}

// 1. Check Docker API Version compatibility
// 2. Check Docker API Version compatibility
clientApiVersion := util.GetDockerClientApiVersion()
serverApiVersion, err := util.GetDockerServerApiVersion(cmd.machine.Name)
serverMinApiVersion, _ := util.GetDockerServerMinApiVersion(cmd.machine.Name)
Expand All @@ -53,7 +76,7 @@ func (cmd *Doctor) Run(c *cli.Context) error {
cmd.out.Error.Printf("Docker Client (%s) is incompatible with Server. Server current (%s), Server min compat (%s). Use `rig upgrade` to fix this.", clientApiVersion, serverApiVersion, serverMinApiVersion)
}

// 2. Pull down the data from DNSDock. This will confirm we can resolve names as well
// 3. Pull down the data from DNSDock. This will confirm we can resolve names as well
// as route to the appropriate IP addresses via the added route commands
if cmd.machine.IsRunning() {
dnsRecords := DnsRecords{BaseCommand{machine: cmd.machine, out: cmd.out}}
Expand All @@ -78,7 +101,7 @@ func (cmd *Doctor) Run(c *cli.Context) error {
cmd.out.Warning.Printf("Docker Machine `%s` is not running. Cannot determine if DNS resolution is working correctly.", cmd.machine.Name)
}

// 3. Ensure that docker-machine-nfs script is available for our NFS mounts (Mac ONLY)
// 4. Ensure that docker-machine-nfs script is available for our NFS mounts (Mac ONLY)
if runtime.GOOS == "darwin" {
if err := exec.Command("which", "docker-machine-nfs").Run(); err != nil {
cmd.out.Error.Println("Docker Machine NFS is not installed.")
Expand All @@ -87,5 +110,35 @@ func (cmd *Doctor) Run(c *cli.Context) error {
}
}

// 5. Check for storage on VM volume
output, err := exec.Command("docker-machine", "ssh", cmd.machine.Name, "df -h 2> /dev/null | grep /dev/sda1 | head -1 | awk '{print $5}' | sed 's/%//'").Output()
dataUsage := strings.TrimSpace(string(output))
if i, err := strconv.Atoi(dataUsage); err == nil {
if i >= 85 && i < 95 {
cmd.out.Warning.Printf("Data volume (/data) is %d%% used. Please free up space soon.", i)
} else if i >= 95 {
cmd.out.Error.Printf("Data volume (/data) is %d%% used. Please free up space. Try 'docker system prune' or removing old projects / databases from /data.", i)
} else {
cmd.out.Info.Printf("Data volume (/data) is %d%% used.", i)
}
} else {
cmd.out.Warning.Printf("Unable to determine usage level of /data volume. Failed to parse '%s'", dataUsage)
}

// 6. Check for storage on /Users
output, err = exec.Command("docker-machine", "ssh", cmd.machine.Name, "df -h 2> /dev/null | grep /Users | head -1 | awk '{print $5}' | sed 's/%//'").Output()
userUsage := strings.TrimSpace(string(output))
if i, err := strconv.Atoi(userUsage); err == nil {
if i >= 85 && i < 95 {
cmd.out.Warning.Printf("Root drive (/Users) is %d%% used. Please free up space soon.", i)
} else if i >= 95 {
cmd.out.Error.Printf("Root drive (/Users) is %d%% used. Please free up space.", i)
} else {
cmd.out.Info.Printf("Root drive (/Users) is %d%% used.", i)
}
} else {
cmd.out.Warning.Printf("Unable to determine usage level of root drive (/Users). Failed to parse '%s'", userUsage)
}

return nil
}
55 changes: 45 additions & 10 deletions cli/commands/project_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ func (cmd *ProjectCreate) Commands() []cli.Command {
Description: "The type is the generator to run with args passed to that generator. If using flag arguments use -- before specifying type and arguments.",
Flags: []cli.Flag{
cli.StringFlag{
Name: "image",
Usage: "Docker image to use if default outrigger/generator is not desired",
Name: "image",
Usage: "Docker image to use if default outrigger/generator is not desired.",
EnvVar: "RIG_PROJECT_CREATE_IMAGE",
},
cli.BoolFlag{
Name: "no-update",
Usage: "Prevent automatic update of designated generator docker image.",
EnvVar: "RIG_PROJECT_CREATE_NO_UPDATE",
},
},
Before: cmd.Before,
Expand All @@ -41,20 +47,49 @@ func (cmd *ProjectCreate) Create(ctx *cli.Context) error {
image = "outrigger/generator"
}

argsMessage := " with no arguments"
if ctx.Args().Present() {
argsMessage = fmt.Sprintf(" with arguments: %s", strings.Join(ctx.Args(), " "))
}

if cmd.machine.IsRunning() {
cmd.out.Verbose.Printf("Executing container %s%s", image, argsMessage)
cmd.RunGenerator(ctx, cmd.machine, image)
} else {
cmd.out.Error.Fatalf("Machine '%s' is not running.", cmd.machine.Name)
}

return nil
}

func (cmd *ProjectCreate) RunGenerator(ctx *cli.Context, machine Machine, image string) error {
machine.SetEnv()

// The check for whether the image is older than 30 days is not currently used.
_, seconds, err := util.ImageOlderThan(image, 86400*30)
if err == nil {
cmd.out.Verbose.Printf("Local copy of the image '%s' was originally published %0.2f days ago.", image, seconds/86400)
}

// If there was an error it implies no previous instance of the image is available
// or that docker operations failed and things will likely go wrong anyway.
if err == nil && !ctx.Bool("no-update") {
cmd.out.Verbose.Printf("Attempting to update %s", image)
if err := util.StreamCommand(exec.Command("docker", "pull", image)); err != nil {
cmd.out.Verbose.Println("Failed to update generator image. Will use local cache if available.")
}
} else if err == nil && ctx.Bool("no-update") {
cmd.out.Verbose.Printf("Automatic generator image update suppressed by --no-update option.")
}

cwd, err := os.Getwd()
if err != nil {
cmd.out.Error.Printf("Couldn't determine current working directory: %s", err)
os.Exit(1)
}

argsMessage := " with no arguments"
if ctx.Args().Present() {
argsMessage = fmt.Sprintf(" with arguments: %s", strings.Join(ctx.Args(), " "))
}
cmd.out.Verbose.Printf("Executing container %s%s", image, argsMessage)

// keep passed in args as distinct elements or they will be treated as
// a single argument containing spaces when the container gets them
// Keep passed in args as distinct elements or they will be treated as
// a single argument containing spaces when the container gets them.
args := []string{
"container",
"run",
Expand Down
71 changes: 55 additions & 16 deletions cli/commands/project_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,24 @@ func (cmd *ProjectSync) Commands() []cli.Command {
Usage: "Start a unison sync on local project directory. Optionally provide a volume name.",
ArgsUsage: "[optional volume name]",
Description: "Volume name will be discovered in the following order: argument to this command > outrigger project config > docker-compose file > current directory name",
Before: cmd.Before,
Action: cmd.RunStart,
Flags: []cli.Flag{
cli.IntFlag{
Name: "initial-sync-timeout",
Value: 60,
Usage: "Maximum amount of time in seconds to allow for detecting each of start of the unison container and start of initial sync",
EnvVar: "RIG_PROJECT_SYNC_TIMEOUT",
},
// Arbitrary sleep length but anything less than 3 wasn't catching
// ongoing very quick file updates during a test
cli.IntFlag{
Name: "initial-sync-wait",
Value: 5,
Usage: "Time in seconds to wait between checks to see if initial sync has finished.",
EnvVar: "RIG_PROJECT_INITIAL_SYNC_WAIT",
},
},
Before: cmd.Before,
Action: cmd.RunStart,
}
stop := cli.Command{
Name: "sync:stop",
Expand Down Expand Up @@ -84,7 +100,7 @@ func (cmd *ProjectSync) RunStart(ctx *cli.Context) error {
cmd.out.Error.Fatalf("Error starting sync container %s: %v", volumeName, err)
}

var ip = cmd.WaitForUnisonContainer(volumeName)
var ip = cmd.WaitForUnisonContainer(volumeName, ctx.Int("initial-sync-timeout"))

cmd.out.Info.Println("Initializing sync")

Expand Down Expand Up @@ -113,7 +129,7 @@ func (cmd *ProjectSync) RunStart(ctx *cli.Context) error {
cmd.out.Error.Fatalf("Error starting local unison process: %v", err)
}

cmd.WaitForSyncInit(logFile)
cmd.WaitForSyncInit(logFile, ctx.Int("initial-sync-timeout"), ctx.Int("initial-sync-wait"))

return nil
}
Expand Down Expand Up @@ -183,56 +199,79 @@ func (cmd *ProjectSync) LoadComposeFile() (*ComposeFile, error) {
// we need to discover the IP address of the container instead of using the DNS name
// when compiled without -cgo this executable will not use the native mac dns resolution
// which is how we have configured dnsdock to provide names for containers.
func (cmd *ProjectSync) WaitForUnisonContainer(containerName string) string {
func (cmd *ProjectSync) WaitForUnisonContainer(containerName string, timeoutSeconds int) string {
cmd.out.Info.Println("Waiting for container to start")

var timeoutLoopSleep = time.Duration(100) * time.Millisecond
// * 10 here because we loop once every 100 ms and we want to get to seconds
var timeoutLoops = timeoutSeconds * 10

output, err := exec.Command("docker", "inspect", "--format", "{{.NetworkSettings.IPAddress}}", containerName).Output()
if err != nil {
cmd.out.Error.Fatalf("Error inspecting sync container %s: %v", containerName, err)
}
ip := strings.Trim(string(output), "\n")

cmd.out.Verbose.Printf("Checking for unison network connection on %s %d", ip, UNISON_PORT)
for i := 1; i <= 100; i++ {
for i := 1; i <= timeoutLoops; i++ {
if conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, UNISON_PORT)); err == nil {
conn.Close()
return ip
} else {
cmd.out.Info.Printf("Error: %v", err)
time.Sleep(time.Duration(100) * time.Millisecond)
time.Sleep(timeoutLoopSleep)
}
}
cmd.out.Error.Fatal("Sync container failed to start!")
return ""
}

// The local unison process is finished initializing when the log file exists
func (cmd *ProjectSync) WaitForSyncInit(logFile string) {
cmd.out.Info.Print("Waiting for initial sync to finish...")
// and has stopped growing in size
func (cmd *ProjectSync) WaitForSyncInit(logFile string, timeoutSeconds int, syncWaitSeconds int) {
cmd.out.Info.Print("Waiting for initial sync detection")

var tempFile = fmt.Sprintf(".%s.tmp", logFile)
var timeoutLoopSleep = time.Duration(100) * time.Millisecond
// * 10 here because we loop once every 100 ms and we want to get to seconds
var timeoutLoops = timeoutSeconds * 10

// Create a temp file to cause a sync action
exec.Command("touch", tempFile).Run()

// Lets check for 60 seconds, while waiting for initial sync to complete
for i := 1; i <= 600; i++ {
for i := 1; i <= timeoutLoops; i++ {
if i%10 == 0 {
os.Stdout.WriteString(".")
}
if _, err := os.Stat(logFile); err == nil {
// Remove the temp file now that we are running
os.Stdout.WriteString("done\n")
if statInfo, err := os.Stat(logFile); err == nil {
os.Stdout.WriteString(" initial sync detected\n")

cmd.out.Info.Print("Waiting for initial sync to finish")
var statSleep = time.Duration(syncWaitSeconds) * time.Second
// Initialize at -2 to force at least one loop
var lastSize = int64(-2)
for lastSize != statInfo.Size() {
os.Stdout.WriteString(".")
time.Sleep(statSleep)
lastSize = statInfo.Size()
if statInfo, err = os.Stat(logFile); err != nil {
cmd.out.Info.Print(err.Error())
lastSize = -1
}
}
os.Stdout.WriteString(" done\n")
// Remove the temp file, waiting until after sync so spurious
// failure message doesn't show in log
exec.Command("rm", "-f", tempFile).Run()
return
} else {
time.Sleep(time.Duration(100) * time.Millisecond)
time.Sleep(timeoutLoopSleep)
}
}

// The log file was not created, the sync has not started yet
exec.Command("rm", "-f", tempFile).Run()
cmd.out.Error.Fatal("Sync container failed to start!")
cmd.out.Error.Fatal("Failed to detect start of initial sync!")
}

// Get the local Unison version to try to load a compatible unison image
Expand Down
Loading

0 comments on commit 05c5997

Please sign in to comment.