Skip to content

Commit 05c5997

Browse files
authored
Merge pull request #63 from phase2/develop
Version 1.3.1
2 parents a55d874 + b4ae58a commit 05c5997

File tree

8 files changed

+276
-89
lines changed

8 files changed

+276
-89
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 1.3.1
4+
5+
- Don't start NFS if not on Darwin
6+
- Auto update generator (project create) and dashboard images
7+
- Added flag to disable autoupdate of generator (project create) image
8+
- Added doctor check for Docker env var configuration
9+
- Added doctor check for `/data` and `/Users` usage
10+
- Added configurable timeouts for sync start
11+
- Added detection when sync start has finished initializing
12+
313
## 1.3.0
414

515
- `Commands()` function now returns an array of cli.Command structs instead of a single struct

cli/commands/dashboard.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func (cmd *Dashboard) Commands() []cli.Command {
2424
}
2525
}
2626

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

44-
dockerApiVersion, _ := util.GetDockerServerApiVersion(cmd.machine.Name)
44+
image := "outrigger/dashboard:latest"
45+
46+
// The check for whether the image is older than 30 days is not currently used.
47+
_, seconds, err := util.ImageOlderThan(image, 86400*30)
48+
if err == nil {
49+
cmd.out.Verbose.Printf("Local copy of the image '%s' was originally published %0.2f days ago.", image, seconds/86400)
50+
}
4551

52+
cmd.out.Verbose.Printf("Attempting to update %s", image)
53+
if err := util.StreamCommand(exec.Command("docker", "pull", image)); err != nil {
54+
cmd.out.Verbose.Println("Failed to update dashboard image. Will use local cache if available.")
55+
}
56+
57+
dockerApiVersion, _ := util.GetDockerServerApiVersion(cmd.machine.Name)
4658
args := []string{
4759
"run",
4860
"-d",

cli/commands/doctor.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package commands
22

33
import (
44
"fmt"
5+
"os"
56
"os/exec"
67
"runtime"
8+
"strings"
79

810
"github.com/hashicorp/go-version"
911
"github.com/phase2/rig/cli/util"
1012
"github.com/urfave/cli"
13+
"strconv"
1114
)
1215

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

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

30-
// 1. Check Docker API Version compatibility
53+
// 2. Check Docker API Version compatibility
3154
clientApiVersion := util.GetDockerClientApiVersion()
3255
serverApiVersion, err := util.GetDockerServerApiVersion(cmd.machine.Name)
3356
serverMinApiVersion, _ := util.GetDockerServerMinApiVersion(cmd.machine.Name)
@@ -53,7 +76,7 @@ func (cmd *Doctor) Run(c *cli.Context) error {
5376
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)
5477
}
5578

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

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

113+
// 5. Check for storage on VM volume
114+
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()
115+
dataUsage := strings.TrimSpace(string(output))
116+
if i, err := strconv.Atoi(dataUsage); err == nil {
117+
if i >= 85 && i < 95 {
118+
cmd.out.Warning.Printf("Data volume (/data) is %d%% used. Please free up space soon.", i)
119+
} else if i >= 95 {
120+
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)
121+
} else {
122+
cmd.out.Info.Printf("Data volume (/data) is %d%% used.", i)
123+
}
124+
} else {
125+
cmd.out.Warning.Printf("Unable to determine usage level of /data volume. Failed to parse '%s'", dataUsage)
126+
}
127+
128+
// 6. Check for storage on /Users
129+
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()
130+
userUsage := strings.TrimSpace(string(output))
131+
if i, err := strconv.Atoi(userUsage); err == nil {
132+
if i >= 85 && i < 95 {
133+
cmd.out.Warning.Printf("Root drive (/Users) is %d%% used. Please free up space soon.", i)
134+
} else if i >= 95 {
135+
cmd.out.Error.Printf("Root drive (/Users) is %d%% used. Please free up space.", i)
136+
} else {
137+
cmd.out.Info.Printf("Root drive (/Users) is %d%% used.", i)
138+
}
139+
} else {
140+
cmd.out.Warning.Printf("Unable to determine usage level of root drive (/Users). Failed to parse '%s'", userUsage)
141+
}
142+
90143
return nil
91144
}

cli/commands/project_create.go

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ func (cmd *ProjectCreate) Commands() []cli.Command {
2323
Description: "The type is the generator to run with args passed to that generator. If using flag arguments use -- before specifying type and arguments.",
2424
Flags: []cli.Flag{
2525
cli.StringFlag{
26-
Name: "image",
27-
Usage: "Docker image to use if default outrigger/generator is not desired",
26+
Name: "image",
27+
Usage: "Docker image to use if default outrigger/generator is not desired.",
28+
EnvVar: "RIG_PROJECT_CREATE_IMAGE",
29+
},
30+
cli.BoolFlag{
31+
Name: "no-update",
32+
Usage: "Prevent automatic update of designated generator docker image.",
33+
EnvVar: "RIG_PROJECT_CREATE_NO_UPDATE",
2834
},
2935
},
3036
Before: cmd.Before,
@@ -41,20 +47,49 @@ func (cmd *ProjectCreate) Create(ctx *cli.Context) error {
4147
image = "outrigger/generator"
4248
}
4349

50+
argsMessage := " with no arguments"
51+
if ctx.Args().Present() {
52+
argsMessage = fmt.Sprintf(" with arguments: %s", strings.Join(ctx.Args(), " "))
53+
}
54+
55+
if cmd.machine.IsRunning() {
56+
cmd.out.Verbose.Printf("Executing container %s%s", image, argsMessage)
57+
cmd.RunGenerator(ctx, cmd.machine, image)
58+
} else {
59+
cmd.out.Error.Fatalf("Machine '%s' is not running.", cmd.machine.Name)
60+
}
61+
62+
return nil
63+
}
64+
65+
func (cmd *ProjectCreate) RunGenerator(ctx *cli.Context, machine Machine, image string) error {
66+
machine.SetEnv()
67+
68+
// The check for whether the image is older than 30 days is not currently used.
69+
_, seconds, err := util.ImageOlderThan(image, 86400*30)
70+
if err == nil {
71+
cmd.out.Verbose.Printf("Local copy of the image '%s' was originally published %0.2f days ago.", image, seconds/86400)
72+
}
73+
74+
// If there was an error it implies no previous instance of the image is available
75+
// or that docker operations failed and things will likely go wrong anyway.
76+
if err == nil && !ctx.Bool("no-update") {
77+
cmd.out.Verbose.Printf("Attempting to update %s", image)
78+
if err := util.StreamCommand(exec.Command("docker", "pull", image)); err != nil {
79+
cmd.out.Verbose.Println("Failed to update generator image. Will use local cache if available.")
80+
}
81+
} else if err == nil && ctx.Bool("no-update") {
82+
cmd.out.Verbose.Printf("Automatic generator image update suppressed by --no-update option.")
83+
}
84+
4485
cwd, err := os.Getwd()
4586
if err != nil {
4687
cmd.out.Error.Printf("Couldn't determine current working directory: %s", err)
4788
os.Exit(1)
4889
}
4990

50-
argsMessage := " with no arguments"
51-
if ctx.Args().Present() {
52-
argsMessage = fmt.Sprintf(" with arguments: %s", strings.Join(ctx.Args(), " "))
53-
}
54-
cmd.out.Verbose.Printf("Executing container %s%s", image, argsMessage)
55-
56-
// keep passed in args as distinct elements or they will be treated as
57-
// a single argument containing spaces when the container gets them
91+
// Keep passed in args as distinct elements or they will be treated as
92+
// a single argument containing spaces when the container gets them.
5893
args := []string{
5994
"container",
6095
"run",

cli/commands/project_sync.go

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,24 @@ func (cmd *ProjectSync) Commands() []cli.Command {
3939
Usage: "Start a unison sync on local project directory. Optionally provide a volume name.",
4040
ArgsUsage: "[optional volume name]",
4141
Description: "Volume name will be discovered in the following order: argument to this command > outrigger project config > docker-compose file > current directory name",
42-
Before: cmd.Before,
43-
Action: cmd.RunStart,
42+
Flags: []cli.Flag{
43+
cli.IntFlag{
44+
Name: "initial-sync-timeout",
45+
Value: 60,
46+
Usage: "Maximum amount of time in seconds to allow for detecting each of start of the unison container and start of initial sync",
47+
EnvVar: "RIG_PROJECT_SYNC_TIMEOUT",
48+
},
49+
// Arbitrary sleep length but anything less than 3 wasn't catching
50+
// ongoing very quick file updates during a test
51+
cli.IntFlag{
52+
Name: "initial-sync-wait",
53+
Value: 5,
54+
Usage: "Time in seconds to wait between checks to see if initial sync has finished.",
55+
EnvVar: "RIG_PROJECT_INITIAL_SYNC_WAIT",
56+
},
57+
},
58+
Before: cmd.Before,
59+
Action: cmd.RunStart,
4460
}
4561
stop := cli.Command{
4662
Name: "sync:stop",
@@ -84,7 +100,7 @@ func (cmd *ProjectSync) RunStart(ctx *cli.Context) error {
84100
cmd.out.Error.Fatalf("Error starting sync container %s: %v", volumeName, err)
85101
}
86102

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

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

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

116-
cmd.WaitForSyncInit(logFile)
132+
cmd.WaitForSyncInit(logFile, ctx.Int("initial-sync-timeout"), ctx.Int("initial-sync-wait"))
117133

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

205+
var timeoutLoopSleep = time.Duration(100) * time.Millisecond
206+
// * 10 here because we loop once every 100 ms and we want to get to seconds
207+
var timeoutLoops = timeoutSeconds * 10
208+
189209
output, err := exec.Command("docker", "inspect", "--format", "{{.NetworkSettings.IPAddress}}", containerName).Output()
190210
if err != nil {
191211
cmd.out.Error.Fatalf("Error inspecting sync container %s: %v", containerName, err)
192212
}
193213
ip := strings.Trim(string(output), "\n")
194214

195215
cmd.out.Verbose.Printf("Checking for unison network connection on %s %d", ip, UNISON_PORT)
196-
for i := 1; i <= 100; i++ {
216+
for i := 1; i <= timeoutLoops; i++ {
197217
if conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, UNISON_PORT)); err == nil {
198218
conn.Close()
199219
return ip
200220
} else {
201221
cmd.out.Info.Printf("Error: %v", err)
202-
time.Sleep(time.Duration(100) * time.Millisecond)
222+
time.Sleep(timeoutLoopSleep)
203223
}
204224
}
205225
cmd.out.Error.Fatal("Sync container failed to start!")
206226
return ""
207227
}
208228

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

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

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

218-
// Lets check for 60 seconds, while waiting for initial sync to complete
219-
for i := 1; i <= 600; i++ {
242+
for i := 1; i <= timeoutLoops; i++ {
220243
if i%10 == 0 {
221244
os.Stdout.WriteString(".")
222245
}
223-
if _, err := os.Stat(logFile); err == nil {
224-
// Remove the temp file now that we are running
225-
os.Stdout.WriteString("done\n")
246+
if statInfo, err := os.Stat(logFile); err == nil {
247+
os.Stdout.WriteString(" initial sync detected\n")
248+
249+
cmd.out.Info.Print("Waiting for initial sync to finish")
250+
var statSleep = time.Duration(syncWaitSeconds) * time.Second
251+
// Initialize at -2 to force at least one loop
252+
var lastSize = int64(-2)
253+
for lastSize != statInfo.Size() {
254+
os.Stdout.WriteString(".")
255+
time.Sleep(statSleep)
256+
lastSize = statInfo.Size()
257+
if statInfo, err = os.Stat(logFile); err != nil {
258+
cmd.out.Info.Print(err.Error())
259+
lastSize = -1
260+
}
261+
}
262+
os.Stdout.WriteString(" done\n")
263+
// Remove the temp file, waiting until after sync so spurious
264+
// failure message doesn't show in log
226265
exec.Command("rm", "-f", tempFile).Run()
227266
return
228267
} else {
229-
time.Sleep(time.Duration(100) * time.Millisecond)
268+
time.Sleep(timeoutLoopSleep)
230269
}
231270
}
232271

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

238277
// Get the local Unison version to try to load a compatible unison image

0 commit comments

Comments
 (0)