Skip to content

Version 1.3.1 #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Aug 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
248b610
Add ImageOlderThan utility function.
Jun 21, 2017
94a075f
Automatically pull generator image before using in project create.
Jun 21, 2017
8481ac1
Automatically pull dashboard image before using in start or dashboard.
Jun 21, 2017
f26158f
Fixed flipped update parameter.
Jun 23, 2017
1ab0e89
Remove explicit name for generator container.
Jun 23, 2017
6345bda
Removed --no-update from rig dashboard.
Jun 27, 2017
f18dd3b
Skip NFS file sharing outside mac OS.
Jun 27, 2017
5c548d2
Unconditionally attempt to update dashboard.
Jun 27, 2017
4e7cbe2
Merge pull request #55 from phase2/skip-nfs-off-osx
febbraro Jun 28, 2017
31060c0
Merge pull request #50 from phase2/auto-update-generator-image
febbraro Jun 28, 2017
4032e69
Add validation that the configured docker-machine is active for rig.
Jun 28, 2017
31d7674
Merge pull request #56 from phase2/doctor-config
febbraro Jul 5, 2017
3597013
Add configurable timeouts for sync:start.
tekante Jul 21, 2017
1fc6552
Format with gofmt
tekante Aug 4, 2017
011542e
Merge pull request #57 from phase2/feature/sync-wait
febbraro Aug 8, 2017
32cf2a1
Improve Linux compatibility and clarity around rig start behaviors.
Aug 11, 2017
8f719c0
Merge pull request #61 from phase2/task/startup-output-and-linux-compat
febbraro Aug 15, 2017
ea2fbe0
Added check for docker env being set at all
febbraro Aug 15, 2017
a434e77
Improves #53 with better sync detection and configuration via environ…
tekante Aug 15, 2017
91dc8a7
added storage checks to doctor
febbraro Aug 15, 2017
9478327
gofmt run on source.
tekante Aug 15, 2017
c0a930a
Reduce number of timeout options and fix error messages.
tekante Aug 15, 2017
7b51d26
Tweak error message as it should only occur when there is no log file.
tekante Aug 15, 2017
3583118
Move comment about wait time to portion where it is relevant now that…
tekante Aug 15, 2017
54de7bd
Move removal of temp file until end of initial sync.
tekante Aug 15, 2017
01df1dc
Merge pull request #62 from phase2/feature/better-sync-detection
febbraro Aug 15, 2017
b4ae58a
Changelog and version bump for 1.3.1
febbraro Aug 15, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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