Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add more details on the doctor additions?

- 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