diff --git a/CHANGELOG.md b/CHANGELOG.md index 1898350..2629a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/cli/commands/dashboard.go b/cli/commands/dashboard.go index f3f0fdd..66659cc 100644 --- a/cli/commands/dashboard.go +++ b/cli/commands/dashboard.go @@ -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) @@ -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", diff --git a/cli/commands/doctor.go b/cli/commands/doctor.go index d3030ff..2cbe153 100644 --- a/cli/commands/doctor.go +++ b/cli/commands/doctor.go @@ -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 { @@ -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) @@ -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}} @@ -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.") @@ -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 } diff --git a/cli/commands/project_create.go b/cli/commands/project_create.go index 039196b..3678a1f 100644 --- a/cli/commands/project_create.go +++ b/cli/commands/project_create.go @@ -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, @@ -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", diff --git a/cli/commands/project_sync.go b/cli/commands/project_sync.go index 28d18cb..6d668d1 100644 --- a/cli/commands/project_sync.go +++ b/cli/commands/project_sync.go @@ -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", @@ -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") @@ -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 } @@ -183,9 +199,13 @@ 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) @@ -193,13 +213,13 @@ func (cmd *ProjectSync) WaitForUnisonContainer(containerName string) string { 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!") @@ -207,32 +227,51 @@ func (cmd *ProjectSync) WaitForUnisonContainer(containerName string) string { } // 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 diff --git a/cli/commands/start.go b/cli/commands/start.go index ec55bf3..17f2464 100644 --- a/cli/commands/start.go +++ b/cli/commands/start.go @@ -2,6 +2,7 @@ package commands import ( "os/exec" + "runtime" "strconv" "github.com/phase2/rig/cli/util" @@ -52,65 +53,79 @@ func (cmd *Start) Commands() []cli.Command { } func (cmd *Start) Run(c *cli.Context) error { - cmd.out.Info.Printf("Starting '%s'", cmd.machine.Name) - cmd.out.Verbose.Println("Pre-flight check...") - - if err := exec.Command("grep", "-qE", "'^\"?/Users/'", "/etc/exports").Run(); err == nil { - cmd.out.Error.Fatal("Vagrant NFS mount found. Please remove any non-Outrigger mounts that begin with /Users from your /etc/exports file") - } - - cmd.out.Verbose.Println("Resetting Docker environment variables...") - cmd.machine.UnsetEnv() - - // Does the docker-machine exist - if !cmd.machine.Exists() { - cmd.out.Warning.Printf("No machine named '%s' exists", cmd.machine.Name) - - driver := c.String("driver") - diskSize := strconv.Itoa(c.Int("disk-size") * 1000) - memSize := strconv.Itoa(c.Int("memory-size")) - cpuCount := strconv.Itoa(c.Int("cpu-count")) - cmd.machine.Create(driver, cpuCount, memSize, diskSize) + if runtime.GOOS == "linux" { + cmd.out.Info.Println("Linux users should use Docker natively for best performance.") + cmd.out.Info.Println("Please ensure your local Docker setup is compatible with Outrigger.") + cmd.out.Info.Println("See http://docs.outrigger.sh/getting-started/linux-installation/") + } else { + cmd.out.Info.Printf("Starting Docker inside a machine with name '%s'", cmd.machine.Name) + cmd.out.Verbose.Println("If something goes wrong, run 'rig doctor'") + cmd.out.Verbose.Println("Pre-flight check...") + + if err := exec.Command("grep", "-qE", "'^\"?/Users/'", "/etc/exports").Run(); err == nil { + cmd.out.Error.Fatal("Vagrant NFS mount found. Please remove any non-Outrigger mounts that begin with /Users from your /etc/exports file") + } + + cmd.out.Verbose.Println("Resetting Docker environment variables...") + cmd.machine.UnsetEnv() + + // Does the docker-machine exist + if !cmd.machine.Exists() { + cmd.out.Warning.Printf("No machine named '%s' exists", cmd.machine.Name) + + driver := c.String("driver") + diskSize := strconv.Itoa(c.Int("disk-size") * 1000) + memSize := strconv.Itoa(c.Int("memory-size")) + cpuCount := strconv.Itoa(c.Int("cpu-count")) + cmd.machine.Create(driver, cpuCount, memSize, diskSize) + } + + cmd.machine.Start() + + cmd.out.Verbose.Println("Configuring the local Docker environment") + cmd.machine.SetEnv() + + cmd.out.Info.Println("Setting up DNS...") + dns := Dns{BaseCommand{machine: cmd.machine, out: cmd.out}} + dns.ConfigureDns(cmd.machine, c.String("nameservers")) + + // NFS mounts are Mac-only. + if runtime.GOOS == "darwin" { + cmd.out.Verbose.Println("Enabling NFS file sharing") + if nfsErr := util.StreamCommand(exec.Command("docker-machine-nfs", cmd.machine.Name)); nfsErr != nil { + cmd.out.Error.Printf("Error enabling NFS: %s", nfsErr) + } + cmd.out.Verbose.Println("NFS is ready to use") + } + + // NFS enabling may have caused a machine restart, wait for it to be available before proceeding + cmd.machine.WaitForDev() + + cmd.out.Verbose.Println("Setting up persistent /data volume...") + dataMountSetup := `if [ ! -d /mnt/sda1/data ]; + then echo '===> Creating /mnt/sda1/data directory'; + sudo mkdir /mnt/sda1/data; + sudo chgrp staff /mnt/sda1/data; + sudo chmod g+w /mnt/sda1/data; + echo '===> Creating /var/lib/boot2docker/bootsync.sh'; + echo '#!/bin/sh' | sudo tee /var/lib/boot2docker/bootsync.sh > /dev/null; + echo 'sudo ln -sf /mnt/sda1/data /data' | sudo tee -a /var/lib/boot2docker/bootsync.sh > /dev/null; + sudo chmod +x /var/lib/boot2docker/bootsync.sh; + fi; + if [ ! -L /data ]; + then echo '===> Creating symlink from /data to /mnt/sda1/data'; + sudo ln -s /mnt/sda1/data /data; + fi;` + util.StreamCommand(exec.Command("docker-machine", "ssh", cmd.machine.Name, dataMountSetup)) + + dns.ConfigureRoutes(cmd.machine) + + cmd.out.Verbose.Println("Use docker-machine to interact with your virtual machine.") + cmd.out.Verbose.Printf("For example, to SSH into it: docker-machine ssh %s", cmd.machine.Name) + cmd.out.Info.Println("To run Docker commands, your terminal session should be initialized with: 'eval \"$(rig config)\"'") } - cmd.machine.Start() - - cmd.out.Verbose.Println("Configuring the local Docker environment") - cmd.machine.SetEnv() - - cmd.out.Info.Println("Setting up DNS...") - dns := Dns{BaseCommand{machine: cmd.machine, out: cmd.out}} - dns.ConfigureDns(cmd.machine, c.String("nameservers")) - - cmd.out.Verbose.Println("Enabling NFS file sharing") - if nfsErr := util.StreamCommand(exec.Command("docker-machine-nfs", cmd.machine.Name)); nfsErr != nil { - cmd.out.Error.Printf("Error enabling NFS: %s", nfsErr) - } - cmd.out.Verbose.Println("NFS is ready to use") - - // NFS enabling may have caused a machine restart, wait for it to be available before proceeding - cmd.machine.WaitForDev() - - cmd.out.Verbose.Println("Setting up persistent /data volume...") - dataMountSetup := `if [ ! -d /mnt/sda1/data ]; - then echo '===> Creating /mnt/sda1/data directory'; - sudo mkdir /mnt/sda1/data; - sudo chgrp staff /mnt/sda1/data; - sudo chmod g+w /mnt/sda1/data; - echo '===> Creating /var/lib/boot2docker/bootsync.sh'; - echo '#!/bin/sh' | sudo tee /var/lib/boot2docker/bootsync.sh > /dev/null; - echo 'sudo ln -sf /mnt/sda1/data /data' | sudo tee -a /var/lib/boot2docker/bootsync.sh > /dev/null; - sudo chmod +x /var/lib/boot2docker/bootsync.sh; - fi; - if [ ! -L /data ]; - then echo '===> Creating symlink from /data to /mnt/sda1/data'; - sudo ln -s /mnt/sda1/data /data; - fi;` - util.StreamCommand(exec.Command("docker-machine", "ssh", cmd.machine.Name, dataMountSetup)) - - dns.ConfigureRoutes(cmd.machine) - - cmd.out.Verbose.Println("Launching Dashboard...") + cmd.out.Info.Println("Launching Dashboard...") dash := Dashboard{BaseCommand{machine: cmd.machine, out: cmd.out}} dash.LaunchDashboard(cmd.machine) diff --git a/cli/main.go b/cli/main.go index 50571f7..79ba54f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -8,7 +8,7 @@ import ( "github.com/urfave/cli" ) -const VERSION = "1.3.0" +const VERSION = "1.3.1" // It all starts here func main() { diff --git a/cli/util/image.go b/cli/util/image.go new file mode 100644 index 0000000..cd5fae7 --- /dev/null +++ b/cli/util/image.go @@ -0,0 +1,23 @@ +package util + +import ( + "os/exec" + "strings" + "time" +) + +func ImageOlderThan(image string, elapsed_seconds float64) (bool, float64, error) { + output, err := exec.Command("docker", "inspect", "--format", "{{.Created}}", image).Output() + if err != nil { + return false, 0, err + } + + datestring := strings.TrimSpace(string(output)) + datetime, err := time.Parse(time.RFC3339, datestring) + if err != nil { + return false, 0, err + } + + seconds := time.Since(datetime).Seconds() + return seconds > elapsed_seconds, seconds, nil +}