Skip to content

Commit

Permalink
Add Spinner implementation for longer-running operations.
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Ross committed Nov 7, 2017
1 parent c561492 commit 5f2c6e7
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 59 deletions.
5 changes: 5 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@
name = "github.com/martinlindhe/notify"
branch = "master"

[[constraint]]
name = "github.com/slok/gospinner"
version = "0.1.0"

11 changes: 11 additions & 0 deletions commands/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type RigCommand interface {
type BaseCommand struct {
RigCommand
out *util.RigLogger
progress *util.RigSpinner
machine Machine
context *cli.Context
}
Expand All @@ -27,6 +28,7 @@ func (cmd *BaseCommand) Before(c *cli.Context) error {
// initialized the logger without the verbose flag if present.
util.LoggerInit(c.GlobalBool("verbose"))
cmd.out = util.Logger()
cmd.progress = util.Spinner()
cmd.machine = Machine{Name: c.GlobalString("name"), out: util.Logger()}

util.NotifyInit(fmt.Sprintf("Outrigger (rig) %s", c.App.Version))
Expand All @@ -39,16 +41,25 @@ func (cmd *BaseCommand) Before(c *cli.Context) error {

// Success encapsulates the functionality for reporting command success
func (cmd *BaseCommand) Success(message string) error {
// Make sure any running spinner halts.
cmd.progress.Spins.Stop()

// Handle success messaging.
if message != "" {
cmd.out.Info.Println(message)
util.NotifySuccess(cmd.context, message)
}

return nil
}

// Error encapsulates the functionality for reporting command failure
func (cmd *BaseCommand) Error(message string, errorName string, exitCode int) error {
// Make sure any running spinner halts.
cmd.progress.Spins.Stop()
// Handle error messaging.
util.NotifyError(cmd.context, message)

return cli.NewExitError(fmt.Sprintf("ERROR: %s [%s] (%d)", message, errorName, exitCode), exitCode)
}

Expand Down
11 changes: 8 additions & 3 deletions commands/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ func (cmd *Dashboard) Commands() []cli.Command {
func (cmd *Dashboard) Run(ctx *cli.Context) error {
if cmd.machine.IsRunning() || util.SupportsNativeDocker() {
cmd.out.Info.Println("Launching Dashboard")
return cmd.LaunchDashboard(cmd.machine)
err := cmd.LaunchDashboard(cmd.machine)
if err != nil {
// Success may be presumed to only execute once per command execution.
// This allows calling LaunchDashboard() from start.go without success.
return cmd.Success("")
}
}

return cmd.Error(fmt.Sprintf("Machine '%s' is not running.", cmd.machine.Name), "MACHINE-STOPPED", 12)
Expand Down Expand Up @@ -71,7 +76,7 @@ func (cmd *Dashboard) LaunchDashboard(machine Machine) error {
dashboardImageName,
}

util.ForceStreamCommand(exec.Command("docker", args...))
util.StreamCommand(exec.Command("docker", args...))

if util.IsMac() {
exec.Command("open", "http://dashboard.outrigger.vm").Run()
Expand All @@ -81,7 +86,7 @@ func (cmd *Dashboard) LaunchDashboard(machine Machine) error {
cmd.out.Info.Println("Outrigger Dashboard is now available at http://dashboard.outrigger.vm")
}

return cmd.Success("")
return nil
}

// StopDashboard stops and removes the dashboard container
Expand Down
4 changes: 3 additions & 1 deletion commands/data_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (cmd *DataBackup) Run(c *cli.Context) error {
return cmd.Error(fmt.Sprintf("Backup archive %s already exists.", backupFile), "BACKUP-ARCHIVE-EXISTS", 12)
}

cmd.out.Info.Printf("Backing up %s on '%s' to %s...", dataDir, cmd.machine.Name, backupFile)
cmd.progress.Start(fmt.Sprintf("Backing up %s on '%s' to %s...", dataDir, cmd.machine.Name, backupFile))

// Stream the archive to stdout and capture it in a local file so we don't waste
// space storing an archive on the VM filesystem. There may not be enough space.
Expand All @@ -76,8 +76,10 @@ func (cmd *DataBackup) Run(c *cli.Context) error {
color.Unset()

if err != nil {
cmd.progress.Fail("Backup failed")
return cmd.Error(err.Error(), "COMMAND-ERROR", 13)
}

cmd.progress.Complete(fmt.Sprintf("Backup complete: %s", backupFile))
return cmd.Success("Data Backup completed with no errors")
}
4 changes: 3 additions & 1 deletion commands/data_restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (cmd *DataRestore) Run(c *cli.Context) error {
return cmd.Error(fmt.Sprintf("Backup archive %s doesn't exists.", backupFile), "BACKUP-ARCHIVE-NOT-FOUND", 12)
}

cmd.out.Info.Printf("Restoring %s to %s on '%s'...", backupFile, dataDir, cmd.machine.Name)
cmd.progress.Start(fmt.Sprintf("Restoring %s to %s on '%s'...", backupFile, dataDir, cmd.machine.Name))

// Send the archive via stdin and extract inline. Saves on disk & performance
extractCmd := fmt.Sprintf("cat %s | docker-machine ssh %s \"sudo tar xzf - -C %s\"", backupFile, cmd.machine.Name, dataDir)
Expand All @@ -72,8 +72,10 @@ func (cmd *DataRestore) Run(c *cli.Context) error {
color.Unset()

if err != nil {
cmd.progress.Fail("Data restore failed")
return cmd.Error(err.Error(), "COMMAND-ERROR", 13)
}

cmd.progress.Complete("Data restore complete")
return cmd.Success("Data Restore was successful")
}
10 changes: 8 additions & 2 deletions commands/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func (cmd *DNS) Run(c *cli.Context) error {
}

if err := cmd.StartDNS(cmd.machine, c.String("nameservers")); err != nil {
cmd.progress.Fail("DNS is ready")
return cmd.Error(err.Error(), "DNS-SETUP-FAILED", 13)
}

Expand All @@ -57,13 +58,15 @@ func (cmd *DNS) Run(c *cli.Context) error {
// ConfigureRoutes will configure routing to allow access to containers on IP addresses
// within the Docker Machine bridge network
func (cmd *DNS) ConfigureRoutes(machine Machine) {
cmd.out.Info.Println("Setting up local networking (may require your admin password)")
cmd.progress.Start("Setting up local networking (may require your admin password)")

if util.IsMac() {
cmd.configureMacRoutes(machine)
} else if util.IsWindows() {
cmd.configureWindowsRoutes(machine)
}

cmd.progress.Complete("Local networking is ready")
}

// ConfigureMac configures DNS resolution and network routing
Expand Down Expand Up @@ -119,6 +122,7 @@ func (cmd *DNS) configureWindowsRoutes(machine Machine) {

// StartDNS will start the dnsdock service
func (cmd *DNS) StartDNS(machine Machine, nameservers string) error {
cmd.progress.Start("Setting up DNS resolver...")
dnsServers := strings.Split(nameservers, ",")

bridgeIP, err := util.GetBridgeIP()
Expand Down Expand Up @@ -149,7 +153,7 @@ func (cmd *DNS) StartDNS(machine Machine, nameservers string) error {
for _, server := range dnsServers {
args = append(args, "--nameserver="+server)
}
util.ForceStreamCommand(exec.Command("docker", args...))
util.StreamCommand(exec.Command("docker", args...))

// Configure the resolvers based on platform
var resolverReturn error
Expand All @@ -160,6 +164,8 @@ func (cmd *DNS) StartDNS(machine Machine, nameservers string) error {
} else if util.IsWindows() {
resolverReturn = cmd.configureWindowsResolver(machine)
}
cmd.progress.Complete("DNS resolution is ready")

return resolverReturn
}

Expand Down
79 changes: 45 additions & 34 deletions commands/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,48 +33,56 @@ func (cmd *Doctor) Commands() []cli.Command {
// nolint: gocyclo
func (cmd *Doctor) Run(c *cli.Context) error {
// 0. Ensure all of rig's dependencies are available in the PATH.
cmd.progress.Start("Checking Docker installation...")
if err := exec.Command("docker", "-h").Start(); err == nil {
cmd.out.Info.Println("Docker is installed.")
cmd.progress.Complete("Docker is installed.")
} else {
cmd.out.Error.Fatal("Docker (docker) is not installed.")
cmd.progress.Fail("Docker (docker) is not installed.")
}
if !util.SupportsNativeDocker() {
cmd.progress.Start("Checking Docker Machine installation...")
if err := exec.Command("docker-machine", "-h").Start(); err == nil {
cmd.out.Info.Println("Docker Machine is installed.")
cmd.progress.Complete("Docker Machine is installed.")
} else {
cmd.out.Error.Fatal("Docker Machine (docker-machine) is not installed.")
cmd.progress.Fail("Docker Machine (docker-machine) is not installed.")
}
}
cmd.progress.Start("Checking Docker Compose installation...")
if err := exec.Command("docker-compose", "-h").Start(); err == nil {
cmd.out.Info.Println("Docker Compose is installed.")
cmd.progress.Complete("Docker Compose is installed.")
} else {
cmd.out.Warning.Printf("Docker Compose (docker-compose) is not installed.")
cmd.progress.Fail("Docker Compose (docker-compose) is not installed.")
}

// 1. Ensure the configured docker-machine matches the set environment.
if !util.SupportsNativeDocker() {
cmd.progress.Start("Checking Docker Machine configuration...")
if cmd.machine.Exists() {
if _, isset := os.LookupEnv("DOCKER_MACHINE_NAME"); !isset {
cmd.out.Error.Fatalf("Docker configuration is not set. Please run 'eval \"$(rig config)\"'.")
cmd.progress.Fail("Docker configuration is not set. Please run 'eval \"$(rig config)\"'.")
return cmd.Error("Could not complete.", "DOCTOR-FATAL", 1)
} 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)
cmd.progress.Fail(fmt.Sprintf("Your environment configuration specifies a different machine. Please re-run as 'rig --name=\"%s\" doctor'.", cmd.machine.Name))
return cmd.Error("Could not complete.", "DOCTOR-FATAL", 1)
} else {
cmd.out.Info.Printf("Docker Machine (%s) name matches your environment configuration.", cmd.machine.Name)
cmd.progress.Complete(fmt.Sprintf("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)
cmd.progress.Fail(fmt.Sprintf("Docker Host configuration should be '%s' but got '%s'. Please re-run 'eval \"$(rig config)\"'.", os.Getenv("DOCKER_HOST"), hostURL))
return cmd.Error("Could not complete.", "DOCTOR-FATAL", 1)
}
cmd.progress.Complete(fmt.Sprintf("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)
cmd.progress.Fail(fmt.Sprintf("No machine named '%s' exists. Did you run 'rig start --name=\"%s\"'?", cmd.machine.Name, cmd.machine.Name))
return cmd.Error("Could not complete.", "DOCTOR-FATAL", 1)
}
}

// 2. Check Docker API Version compatibility
cmd.progress.Start("Checking Docker version...")
if !util.SupportsNativeDocker() {
clientAPIVersion := util.GetDockerClientAPIVersion()
serverAPIVersion, err := util.GetDockerServerAPIVersion(cmd.machine.Name)
Expand All @@ -92,88 +100,91 @@ func (cmd *Doctor) Run(c *cli.Context) error {
apiConstraint, _ := version.NewConstraint(constraintString)

if err != nil {
cmd.out.Error.Println("Could not determine Docker Machine Docker versions: ", err)
cmd.progress.Fail(fmt.Sprintln("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)
cmd.progress.Complete(fmt.Sprintf("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)
cmd.progress.Complete(fmt.Sprintf("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)
cmd.progress.Fail(fmt.Sprintf("Docker Client (%s) is incompatible with Server. Server current (%s), Server min compat (%s). Use `rig upgrade` to fix this.", clientAPIVersion, serverAPIVersion, serverMinAPIVersion))
}
} else {
dockerAPIVersion := util.GetDockerClientAPIVersion()
cmd.out.Info.Printf("Docker API Version: %s", dockerAPIVersion)
cmd.progress.Complete(fmt.Sprintln("Docker API Version:", 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
cmd.progress.Start("Checking DNS configuration...")
dnsRecords := DNSRecords{cmd.BaseCommand}
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"])
cmd.progress.Complete(fmt.Sprintf("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.")
cmd.progress.Fail("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)
cmd.progress.Fail(fmt.Sprintf("Unable to verify DNS services and routing are working: %s", err.Error()))
}

// 4. Ensure that docker-machine-nfs script is available for our NFS mounts (Mac ONLY)
if util.IsMac() {
cmd.progress.Start("Checking NFS configuration...")
if err := exec.Command("which", "docker-machine-nfs").Run(); err != nil {
cmd.out.Error.Println("Docker Machine NFS is not installed.")
cmd.progress.Fail("Docker Machine NFS is not installed.")
} else {
cmd.out.Info.Println("Docker Machine NFS is installed.")
cmd.progress.Complete("Docker Machine NFS is installed.")
}
}

// 5. Check for storage on VM volume
if !util.SupportsNativeDocker() {
cmd.progress.Start("Checking Data (/data) volume capacity...")
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, e := strconv.Atoi(dataUsage); e == nil {
if i >= 85 && i < 95 {
cmd.out.Warning.Printf("Data volume (/data) is %d%% used. Please free up space soon.", i)
cmd.progress.Warn(fmt.Sprintf("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)
cmd.progress.Fail(fmt.Sprintf("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)
cmd.progress.Complete(fmt.Sprintf("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)
cmd.progress.Warn(fmt.Sprintf("Unable to determine usage level of /data volume. Failed to parse '%s'", dataUsage))
}
} else {
cmd.out.Warning.Printf("Unable to determine usage level of /data volume. Failed to execute 'df': %v", err)
cmd.progress.Warn(fmt.Sprintf("Unable to determine usage level of /data volume. Failed to execute 'df': %v", err))
}
}

// 6. Check for storage on /Users
if !util.SupportsNativeDocker() {
cmd.progress.Start("Checking Root (/Users) drive capacity...")
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, e := strconv.Atoi(userUsage); e == nil {
if i >= 85 && i < 95 {
cmd.out.Warning.Printf("Root drive (/Users) is %d%% used. Please free up space soon.", i)
cmd.progress.Warn(fmt.Sprintf("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)
cmd.progress.Fail(fmt.Sprintf("Root drive (/Users) is %d%% used. Please free up space.", i))
} else {
cmd.out.Info.Printf("Root drive (/Users) is %d%% used.", i)
cmd.progress.Complete(fmt.Sprintf("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)
cmd.progress.Warn(fmt.Sprintf("Unable to determine usage level of root drive (/Users). Failed to parse '%s'", userUsage))
}
} else {
cmd.out.Warning.Printf("Unable to determine usage level of root drive (/Users). Failed to execute 'df': %v", err)
cmd.progress.Warn(fmt.Sprintf("Unable to determine usage level of root drive (/Users). Failed to execute 'df': %v", err))
}
}

Expand Down
6 changes: 4 additions & 2 deletions commands/project_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ func (cmd *ProjectCreate) RunGenerator(ctx *cli.Context, machine Machine, image
// 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)
cmd.progress.Start(fmt.Sprintf("Attempting to update project generator docker image: %s", image))
if e := util.StreamCommand(exec.Command("docker", "pull", image)); e != nil {
cmd.out.Verbose.Println("Failed to update generator image. Will use local cache if available.")
cmd.progress.Fail(fmt.Sprintf("Project generator docker image failed to update. Using local cache if available: %s", image))
} else {
cmd.progress.Complete(fmt.Sprintf("Project generator docker image is up-to-date: %s", image))
}
} else if err == nil && ctx.Bool("no-update") {
cmd.out.Verbose.Printf("Automatic generator image update suppressed by --no-update option.")
Expand Down
Loading

0 comments on commit 5f2c6e7

Please sign in to comment.