Skip to content

Commit

Permalink
Merge pull request #77 from phase2/develop
Browse files Browse the repository at this point in the history
1.3.2 Release
  • Loading branch information
febbraro authored Oct 10, 2017
2 parents 05c5997 + f76dd23 commit d1a9e76
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 85 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.3.2

- Added support for outrigger.yml (non-hidden)
- Added Linux compatibility to `doctor`
- Added support for Linux local bind volumes (for parity with `sync:start`)

## 1.3.1

- Don't start NFS if not on Darwin
Expand Down
176 changes: 105 additions & 71 deletions cli/commands/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,76 +29,98 @@ 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)
// 0. Ensure all of rig's dependencies are available in the PATH.
if err := exec.Command("docker", "-h").Start(); err == nil {
cmd.out.Info.Println("Docker is installed.")
} else {
cmd.out.Error.Fatal("Docker (docker) is not installed.")
}
if runtime.GOOS != "linux" {
if err := exec.Command("docker-machine", "-h").Start(); err == nil {
cmd.out.Info.Println("Docker Machine is installed.")
} else {
cmd.out.Info.Printf("Docker Machine (%s) name matches your environment configuration.", cmd.machine.Name)
cmd.out.Error.Fatal("Docker Machine (docker-machine) is not installed.")
}
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)
}
if err := exec.Command("docker-compose", "-h").Start(); err == nil {
cmd.out.Info.Println("Docker Compose is installed.")
} else {
cmd.out.Warning.Printf("Docker Compose (docker-compose) is not installed.")
}

// 1. Ensure the configured docker-machine matches the set environment.
if runtime.GOOS != "linux" {
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) URL (%s) matches your environment configuration.", cmd.machine.Name, hostUrl)
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)
}
} else {
cmd.out.Error.Fatalf("No machine named '%s' exists. Did you run 'rig start --name=\"%s\"'?", cmd.machine.Name, cmd.machine.Name)
}

// 2. Check Docker API Version compatibility
clientApiVersion := util.GetDockerClientApiVersion()
serverApiVersion, err := util.GetDockerServerApiVersion(cmd.machine.Name)
serverMinApiVersion, _ := util.GetDockerServerMinApiVersion(cmd.machine.Name)
if runtime.GOOS != "linux" {
clientApiVersion := util.GetDockerClientApiVersion()
serverApiVersion, err := util.GetDockerServerApiVersion(cmd.machine.Name)
serverMinApiVersion, _ := util.GetDockerServerMinApiVersion(cmd.machine.Name)

// Older clients can talk to newer servers, and when you ask a newer server
// it's version in the presence of an older server it will downgrade it's
// compatability as far as possible. So as long as the client API is not greater
// than the servers current version or less than the servers minimum api version
// then we are compatible
constraintString := fmt.Sprintf("<= %s", serverApiVersion)
if serverMinApiVersion != nil {
constraintString = fmt.Sprintf(">= %s", serverMinApiVersion)
}
apiConstraint, _ := version.NewConstraint(constraintString)
// Older clients can talk to newer servers, and when you ask a newer server
// it's version in the presence of an older server it will downgrade it's
// compatability as far as possible. So as long as the client API is not greater
// than the servers current version or less than the servers minimum api version
// then we are compatible
constraintString := fmt.Sprintf("<= %s", serverApiVersion)
if serverMinApiVersion != nil {
constraintString = fmt.Sprintf(">= %s", serverMinApiVersion)
}
apiConstraint, _ := version.NewConstraint(constraintString)

if err != nil {
cmd.out.Error.Println("Could not determine Docker Machine Docker versions: ", err)
} else if clientApiVersion.Equal(serverApiVersion) {
cmd.out.Info.Printf("Docker Client (%s) and Server (%s) have equal API Versions", clientApiVersion, serverApiVersion)
} else if apiConstraint.Check(clientApiVersion) {
cmd.out.Info.Printf("Docker Client (%s) has Server compatible API version (%s). Server current (%s), Server min compat (%s)", clientApiVersion, constraintString, serverApiVersion, serverMinApiVersion)
if err != nil {
cmd.out.Error.Println("Could not determine Docker Machine Docker versions: ", err)
} else if clientApiVersion.Equal(serverApiVersion) {
cmd.out.Info.Printf("Docker Client (%s) and Server (%s) have equal API Versions", clientApiVersion, serverApiVersion)
} else if apiConstraint.Check(clientApiVersion) {
cmd.out.Info.Printf("Docker Client (%s) has Server compatible API version (%s). Server current (%s), Server min compat (%s)", clientApiVersion, constraintString, serverApiVersion, serverMinApiVersion)
} else {
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)
}
} else {
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)
dockerApiVersion := util.GetDockerClientApiVersion()
cmd.out.Info.Printf("Docker API Version: %s", dockerApiVersion)
}

// 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}}
if records, err := dnsRecords.LoadRecords(); err == nil {
resolved := false
for _, record := range records {
if record["Name"] == "dnsdock" {
resolved = true
cmd.out.Info.Printf("DNS and routing services are working. DNSDock resolves to %s", record["IPs"])
break
}
dnsRecords := DnsRecords{BaseCommand{machine: cmd.machine, out: cmd.out}}
if records, err := dnsRecords.LoadRecords(); err == nil {
resolved := false
for _, record := range records {
if record["Name"] == "dnsdock" {
resolved = true
cmd.out.Info.Printf("DNS and routing services are working. DNSDock resolves to %s", record["IPs"])
break
}
}

if !resolved {
cmd.out.Error.Println("Unable to verify DNS services are working.")
}
} else {
cmd.out.Error.Println("Unable to verify DNS services and routing are working.")
cmd.out.Error.Println(err)
if !resolved {
cmd.out.Error.Println("Unable to verify DNS services are working.")
}
} else {
cmd.out.Warning.Printf("Docker Machine `%s` is not running. Cannot determine if DNS resolution is working correctly.", cmd.machine.Name)
cmd.out.Error.Println("Unable to verify DNS services and routing are working.")
cmd.out.Error.Println(err)
}

// 4. Ensure that docker-machine-nfs script is available for our NFS mounts (Mac ONLY)
Expand All @@ -111,33 +133,45 @@ 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)
if runtime.GOOS != "linux" {
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()
if err == nil {
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)
}
} else {
cmd.out.Info.Printf("Data volume (/data) is %d%% used.", i)
cmd.out.Warning.Printf("Unable to determine usage level of /data volume. Failed to execute 'df': %v", err)
}
} 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)
if runtime.GOOS != "linux" {
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()
if err == nil {
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)
}
} else {
cmd.out.Info.Printf("Root drive (/Users) is %d%% used.", i)
cmd.out.Warning.Printf("Unable to determine usage level of root drive (/Users). Failed to execute 'df': %v", err)
}
} else {
cmd.out.Warning.Printf("Unable to determine usage level of root drive (/Users). Failed to parse '%s'", userUsage)
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion cli/commands/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (cmd *Project) Commands() []cli.Command {
command := cli.Command{
Name: "project",
Usage: "Run project-specific commands.",
Description: "Run project-specific commands as part of development.\n\n\tConfigured scripts are driven by an Outrigger configuration file expected at your project root directory.\n\n\tBy default, this is a YAML file named '.outrigger.yml'. It can be overridden by setting an environment variable $RIG_PROJECT_CONFIG_FILE.",
Description: "Run project-specific commands as part of development.\n\n\tConfigured scripts are driven by an Outrigger configuration file expected at your project root directory.\n\n\tBy default, this is a YAML file named 'outrigger.yml' with fallback to '.outrigger.yml'. It can be overridden by setting an environment variable $RIG_PROJECT_CONFIG_FILE.",
Aliases: []string{"run"},
Category: "Development",
Before: cmd.Before,
Expand Down
26 changes: 21 additions & 5 deletions cli/commands/project_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,30 @@ type ProjectConfig struct {
// Create a new ProjectConfig using configured or default locations
func NewProjectConfig() *ProjectConfig {
projectConfigFile := os.Getenv("RIG_PROJECT_CONFIG_FILE")

var discovery []string
if projectConfigFile == "" {
projectConfigFile = "./.outrigger.yml"
discovery = make([]string, 2)
discovery[0] = "./outrigger.yml"
discovery[1] = "./.outrigger.yml"
} else {
discovery = make([]string, 1)
discovery[0] = projectConfigFile
}
return NewProjectConfigFromFile(projectConfigFile)

readyConfig := &ProjectConfig{}
for _, filePath := range discovery {
if config, err := NewProjectConfigFromFile(filePath); err == nil {
readyConfig = config
break
}
}

return readyConfig
}

// Create a new ProjectConfig from the specified file
func NewProjectConfigFromFile(filename string) *ProjectConfig {
func NewProjectConfigFromFile(filename string) (*ProjectConfig, error) {
logger := util.Logger()

filepath, _ := filepath.Abs(filename)
Expand All @@ -55,7 +71,7 @@ func NewProjectConfigFromFile(filename string) *ProjectConfig {
yamlFile, err := ioutil.ReadFile(config.File)
if err != nil {
logger.Verbose.Printf("No project configuration file found at: %s", config.File)
return config
return config, err
}

if err := yaml.Unmarshal(yamlFile, config); err != nil {
Expand All @@ -76,7 +92,7 @@ func NewProjectConfigFromFile(filename string) *ProjectConfig {
}
}

return config
return config, nil
}

// Ensures our configuration data structure conforms to our ad hoc schema.
Expand Down
50 changes: 43 additions & 7 deletions cli/commands/project_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"path"
"regexp"
"runtime"
"strings"
"time"

Expand Down Expand Up @@ -43,15 +44,15 @@ func (cmd *ProjectSync) Commands() []cli.Command {
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",
Usage: "Maximum amount of time in seconds to allow for detecting each of start of the unison container and start of initial sync. (not needed on linux)",
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.",
Usage: "Time in seconds to wait between checks to see if initial sync has finished. (not needed on linux)",
EnvVar: "RIG_PROJECT_INITIAL_SYNC_WAIT",
},
},
Expand All @@ -74,8 +75,21 @@ func (cmd *ProjectSync) Commands() []cli.Command {
func (cmd *ProjectSync) RunStart(ctx *cli.Context) error {
config := NewProjectConfig()
volumeName := cmd.GetVolumeName(ctx, config)
cmd.out.Verbose.Printf("Starting sync with volume: %s", volumeName)

switch platform := runtime.GOOS; platform {
case "linux":
cmd.out.Verbose.Printf("Setting up local volume: %s", volumeName)
cmd.SetupBindVolume(volumeName)
default:
cmd.out.Verbose.Printf("Starting sync with volume: %s", volumeName)
cmd.StartUnisonSync(ctx, volumeName, config)
}

return nil
}

// For systems that need/support Unison
func (cmd *ProjectSync) StartUnisonSync(ctx *cli.Context, volumeName string, config *ProjectConfig) {
// Ensure the processes can handle a large number of watches
if err := cmd.machine.SetSysctl("fs.inotify.max_user_watches", MAX_WATCHES); err != nil {
cmd.out.Error.Fatalf("Error configuring file watches on Docker Machine: %v", err)
Expand All @@ -86,6 +100,7 @@ func (cmd *ProjectSync) RunStart(ctx *cli.Context) error {

cmd.out.Info.Println("Starting unison container")
unisonMinorVersion := cmd.GetUnisonMinorVersion()

cmd.out.Verbose.Printf("Local unison version for compatibilty: %s", unisonMinorVersion)
exec.Command("docker", "container", "stop", volumeName).Run()
err := exec.Command("docker", "container", "run", "--detach", "--rm",
Expand All @@ -96,6 +111,7 @@ func (cmd *ProjectSync) RunStart(ctx *cli.Context) error {
"--name", volumeName,
fmt.Sprintf("outrigger/unison:%s", unisonMinorVersion),
).Run()

if err != nil {
cmd.out.Error.Fatalf("Error starting sync container %s: %v", volumeName, err)
}
Expand Down Expand Up @@ -123,19 +139,39 @@ func (cmd *ProjectSync) RunStart(ctx *cli.Context) error {
unisonArgs = append(unisonArgs, "-ignore", ignore)
}
}

cmd.out.Verbose.Printf("Unison Args: %s", strings.Join(unisonArgs[:], " "))
if err = exec.Command("unison", unisonArgs...).Start(); err != nil {
cmd.out.Error.Fatalf("Error starting local unison process: %v", err)
}

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

return nil
// For systems that have native container/volume support
func (cmd *ProjectSync) SetupBindVolume(volumeName string) {

cmd.out.Info.Printf("Starting local bind volume: %s", volumeName)
exec.Command("docker", "volume", "rm", volumeName).Run()

if workingDir, err := os.Getwd(); err == nil {
volumeArgs := []string{
"volume", "create",
"--opt", "type=none",
"--opt", fmt.Sprintf("device=%s", workingDir),
"--opt", "o=bind",
volumeName,
}
exec.Command("docker", volumeArgs...).Run()
} else {
cmd.out.Error.Fatalf("Error resolving the working directory for volume creation: %v", err)
}
}

// Start the unison sync process
func (cmd *ProjectSync) RunStop(ctx *cli.Context) error {
if runtime.GOOS == "linux" {
cmd.out.Info.Println("No unison container to stop, using local bind volume")
return nil
}

config := NewProjectConfig()
volumeName := cmd.GetVolumeName(ctx, config)
cmd.out.Verbose.Printf("Stopping sync with volume: %s", volumeName)
Expand Down
Loading

0 comments on commit d1a9e76

Please sign in to comment.