func sshCreate(ctx *cli.Context) { if len(ctx.Args()) != 2 { log.Fatal("Must provide name and public key file.") } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) file, err := os.Open(ctx.Args()[1]) if err != nil { log.Fatalf("Error opening key file: %s.", err) } keyData, err := ioutil.ReadAll(file) if err != nil { log.Fatalf("Error reading key file: %s.", err) } createRequest := &godo.KeyCreateRequest{ Name: ctx.Args().First(), PublicKey: string(keyData), } key, _, err := client.Keys.Create(createRequest) if err != nil { log.Fatal(err) } WriteOutput(key) }
func TestApp_RunAsSubcommandParseFlags(t *testing.T) { var context *cli.Context a := cli.NewApp() a.Commands = []cli.Command{ { Name: "foo", Action: func(c *cli.Context) { context = c }, Flags: []cli.Flag{ cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", }, }, Before: func(_ *cli.Context) error { return nil }, }, } a.Run([]string{"", "foo", "--lang", "spanish", "abcd"}) expect(t, context.Args().Get(0), "abcd") expect(t, context.String("lang"), "spanish") }
func sizeList(ctx *cli.Context) { if ctx.BoolT("help") == true { cli.ShowAppHelp(ctx) os.Exit(1) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) opt := &godo.ListOptions{ Page: 1, PerPage: 50, // Not likely to have more than 50 sizes soon } sizeList, _, err := client.Sizes.List(opt) if err != nil { fmt.Printf("Unable to list Sizes: %s\n", err) os.Exit(1) } cliOut := NewCLIOutput() defer cliOut.Flush() cliOut.Header("Slug", "Memory", "VCPUs", "Disk", "Transfer", "Price Monthly", "Price Hourly") for _, size := range sizeList { cliOut.Writeln("%s\t%dMB\t%d\t%dGB\t%d\t$%.0f\t$%.5f\n", size.Slug, size.Memory, size.Vcpus, size.Disk, size.Transfer, size.PriceMonthly, size.PriceHourly) } }
func regionList(ctx *cli.Context) { if ctx.BoolT("help") == true { cli.ShowAppHelp(ctx) os.Exit(1) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) opt := &godo.ListOptions{ Page: 1, PerPage: 50, // Not likely to have more than 50 regions soon } regionList, _, err := client.Regions.List(opt) if err != nil { fmt.Printf("Unable to list Regions: %s\n", err) os.Exit(1) } cliOut := NewCLIOutput() defer cliOut.Flush() cliOut.Header("Name", "Slug", "Available") for _, region := range regionList { cliOut.Writeln("%s\t%s\t%t\n", region.Name, region.Slug, region.Available) } }
func dropletActionSnapshot(ctx *cli.Context) { if ctx.Int("id") == 0 && len(ctx.Args()) != 1 { log.Fatal("Error: Must provide ID or name for Droplet to resize.") } name := ctx.String("name") tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) id := ctx.Int("id") if id == 0 { droplet, err := FindDropletByName(client, ctx.Args()[0]) if err != nil { log.Fatal(err) } else { id = droplet.ID } } droplet, _, err := client.Droplets.Get(id) if err != nil { log.Fatal("Unable to find Droplet: %s.", err) } action, _, err := client.DropletActions.Snapshot(droplet.ID, name) if err != nil { log.Fatal(err) } WriteOutput(action) }
func authMethods(ctx *cli.Context) (methods []ssh.AuthMethod, err error) { reader := bufio.NewReader(os.Stdin) fmt.Print("Password: "******"key") if keyPath == "" { keyPath = filepath.Join(usr.HomeDir, ".ssh", "id_rsa") } key, err := ioutil.ReadFile(keyPath) if err != nil { return } privateKey, err := ssh.ParsePrivateKey(key) if err != nil { return } methods = append(methods, ssh.PublicKeys(privateKey)) return }
func dropletList(ctx *cli.Context) { if ctx.BoolT("help") == true { cli.ShowAppHelp(ctx) os.Exit(1) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) opt := &godo.ListOptions{} dropletList := []godo.Droplet{} for { // TODO make all optional dropletPage, resp, err := client.Droplets.List(opt) if err != nil { fmt.Printf("Unable to list Droplets: %s\n", err) os.Exit(1) } // append the current page's droplets to our list for _, d := range dropletPage { dropletList = append(dropletList, d) } // if we are at the last page, break out the for loop if resp.Links == nil || resp.Links.IsLastPage() { break } page, err := resp.Links.CurrentPage() if err != nil { fmt.Printf("Unable to get pagination: %s\n", err) os.Exit(1) } // set the page we want for the next request opt.Page = page + 1 } cliOut := NewCLIOutput() defer cliOut.Flush() cliOut.Header("ID", "Name", "IP Address", "Status", "Memory", "Disk", "Region") for _, droplet := range dropletList { publicIP := PublicIPForDroplet(&droplet) cliOut.Writeln("%d\t%s\t%s\t%s\t%dMB\t%dGB\t%s\n", droplet.ID, droplet.Name, publicIP, droplet.Status, droplet.Memory, droplet.Disk, droplet.Region.Slug) } }
func domainRecordList(ctx *cli.Context) { if len(ctx.Args()) != 1 { fmt.Printf("Error: Must provide a domain name for which to list records.\n") os.Exit(64) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) domainName := ctx.Args().First() opt := &godo.ListOptions{ Page: ctx.Int("page"), PerPage: ctx.Int("page-size"), } domainDecords, _, err := client.Domains.Records(domainName, opt) if err != nil { fmt.Printf("%s\n", err) os.Exit(1) } WriteOutput(domainDecords) }
func dropletDestroy(ctx *cli.Context) { if ctx.Int("id") == 0 && len(ctx.Args()) != 1 { log.Fatal("Error: Must provide ID or name for Droplet to destroy.") } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) id := ctx.Int("id") if id == 0 { droplet, err := FindDropletByName(client, ctx.Args()[0]) if err != nil { log.Fatal(err) } else { id = droplet.ID } } droplet, _, err := client.Droplets.Get(id) if err != nil { log.Fatalf("Unable to find Droplet: %s.", err) } _, err = client.Droplets.Delete(id) if err != nil { log.Fatalf("Unable to destroy Droplet: %s.", err) } log.Fatalf("Droplet %s destroyed.", droplet.Name) }
func sshList(ctx *cli.Context) { if ctx.BoolT("help") == true { cli.ShowAppHelp(ctx) os.Exit(1) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) opt := &godo.ListOptions{} keyList := []godo.Key{} for { keyPage, resp, err := client.Keys.List(opt) if err != nil { fmt.Printf("Unable to list SSH Keys: %s\n", err) os.Exit(1) } // append the current page's droplets to our list for _, d := range keyPage { keyList = append(keyList, d) } // if we are at the last page, break out the for loop if resp.Links == nil || resp.Links.IsLastPage() { break } page, err := resp.Links.CurrentPage() if err != nil { fmt.Printf("Unable to get pagination: %s\n", err) os.Exit(1) } // set the page we want for the next request opt.Page = page + 1 } cliOut := NewCLIOutput() defer cliOut.Flush() cliOut.Header("ID", "Name", "Fingerprint") for _, key := range keyList { cliOut.Writeln("%d\t%s\t%s\n", key.ID, key.Name, key.Fingerprint) } }
func domainShow(ctx *cli.Context) { if len(ctx.Args()) != 1 { log.Fatal("Error: Must provide name for Domain.") } name := ctx.Args().First() tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) domain, _, err := client.Domains.Get(name) if err != nil { log.Fatal(err) } WriteOutput(domain) }
func domainDestroy(ctx *cli.Context) { if len(ctx.Args()) != 1 { log.Fatal("Error: Must provide a name for the domain to destroy.") } name := ctx.Args().First() tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) _, err := client.Domains.Delete(name) if err != nil { log.Fatalf("Unable to destroy domain: %s.", err) } log.Printf("Domain %s destroyed", name) }
func sshFind(ctx *cli.Context) { if len(ctx.Args()) != 1 { log.Fatal("Error: Must provide name for Key.") } name := ctx.Args().First() tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) key, err := FindKeyByName(client, name) if err != nil { log.Fatal(err) } WriteOutput(key) }
func domainRecordDestroy(ctx *cli.Context) { if len(ctx.Args()) != 2 { fmt.Printf("Error: Must provide domain name and domain record id.\n") os.Exit(1) } domainName := ctx.Args().First() recordID, err := strconv.Atoi(ctx.Args()[1]) if err != nil { fmt.Printf("%s\n", err) os.Exit(1) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) _, err = client.Domains.DeleteRecord(domainName, recordID) if err != nil { fmt.Printf("Unable to destroy domain record: %s\n", err) os.Exit(1) } fmt.Printf("Domain record %d destroyed.\n", recordID) }
func domainCreate(ctx *cli.Context) { if len(ctx.Args()) != 2 { log.Fatal("Must provide domain name and Droplet name.") } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) droplet, err := FindDropletByName(client, ctx.Args()[1]) if err != nil { log.Fatal(err) } createRequest := &godo.DomainCreateRequest{ Name: ctx.Args().First(), IPAddress: PublicIPForDroplet(droplet), } domain, _, err := client.Domains.Create(createRequest) if err != nil { log.Fatal(err) } WriteOutput(domain) }
func domainList(ctx *cli.Context) { if ctx.BoolT("help") == true { cli.ShowAppHelp(ctx) os.Exit(1) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) opt := &godo.ListOptions{ Page: ctx.Int("page"), PerPage: ctx.Int("page-size"), } domainList, _, err := client.Domains.List(opt) if err != nil { log.Fatalf("Unable to list Domains: %s.", err) } cliOut := NewCLIOutput() defer cliOut.Flush() cliOut.Header("Name", "TTL") for _, domain := range domainList { cliOut.Writeln("%s\t%d\n", domain.Name, domain.TTL) } }
func actionList(ctx *cli.Context) { if ctx.BoolT("help") == true { cli.ShowAppHelp(ctx) os.Exit(1) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) opt := &godo.ListOptions{ Page: ctx.Int("page"), PerPage: ctx.Int("page-size"), } actionList, _, err := client.Actions.List(opt) if err != nil { fmt.Printf("Unable to list Actions: %s\n", err) os.Exit(1) } cliOut := NewCLIOutput() defer cliOut.Flush() cliOut.Header("ID", "Region", "ResourceType", "ResourceID", "Type", "StartedAt", "CompletedAt", "Status") for _, action := range actionList { cliOut.Writeln("%d\t%s\t%s\t%d\t%s\t%s\t%s\t%s\n", action.ID, action.RegionSlug, action.ResourceType, action.ResourceID, action.Type, action.StartedAt, action.CompletedAt, action.Status) } }
func domainRecordShow(ctx *cli.Context) { if len(ctx.Args()) == 2 { fmt.Printf("Error: Must provide domain name and domain record id.\n") os.Exit(64) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) domainName := ctx.Args().First() recordID, err := strconv.Atoi(ctx.Args()[1]) if err != nil { fmt.Printf("%s\n", err) os.Exit(1) } domainDecord, _, err := client.Domains.Record(domainName, recordID) if err != nil { fmt.Printf("%s\n", err) os.Exit(1) } WriteOutput(domainDecord) }
func domainShow(ctx *cli.Context) { if len(ctx.Args()) != 1 { fmt.Printf("Error: Must provide name for Domain.\n") os.Exit(64) } name := ctx.Args().First() tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) domain, _, err := client.Domains.Get(name) if err != nil { fmt.Printf("%s\n", err) os.Exit(1) } WriteOutput(domain) }
func sshFind(ctx *cli.Context) { if len(ctx.Args()) != 1 { fmt.Printf("Error: Must provide name for Key.\n") os.Exit(1) } name := ctx.Args().First() tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) key, err := FindKeyByName(client, name) if err != nil { fmt.Printf("%s\n", err) os.Exit(64) } WriteOutput(key) }
func dropletActionPasswordReset(ctx *cli.Context) { if ctx.Int("id") == 0 && len(ctx.Args()) != 1 { fmt.Printf("Error: Must provide ID or name for Droplet to destroy.\n") os.Exit(1) } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) id := ctx.Int("id") if id == 0 { droplet, err := FindDropletByName(client, ctx.Args()[0]) if err != nil { fmt.Printf("%s\n", err) os.Exit(64) } else { id = droplet.ID } } droplet, _, err := client.Droplets.Get(id) if err != nil { fmt.Printf("Unable to find Droplet: %s\n", err) os.Exit(1) } action, _, err := client.DropletActions.PasswordReset(droplet.ID) if err != nil { fmt.Println(err) os.Exit(1) } WriteOutput(action) }
func dropletFind(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { log.Fatal("Error: Must provide one name for a Droplet search.") } name := ctx.Args().First() tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) droplet, err := FindDropletByName(client, name) if err != nil { log.Fatal(err) } WriteOutput(droplet) }
func actionShow(ctx *cli.Context) { if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 { log.Fatal("Error: Must provide exactly one id of an Action to show.") } id, _ := strconv.ParseInt(ctx.Args().First(), 10, 0) tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) action, _, err := client.Actions.Get(int(id)) if err != nil { log.Fatal(err) } WriteOutput(action) }
func sshDestroy(ctx *cli.Context) { if ctx.Int("id") == 0 && ctx.String("fingerprint") == "" && len(ctx.Args()) < 1 { log.Fatal("Error: Must provide ID, fingerprint or name for SSH Key to destroy.") } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) id := ctx.Int("id") fingerprint := ctx.String("fingerprint") var key godo.Key if id == 0 && fingerprint == "" { key, err := FindKeyByName(client, ctx.Args().First()) if err != nil { log.Fatal(err) } else { id = key.ID } } else if id != 0 { key, _, err := client.Keys.GetByID(id) if err != nil { log.Fatalf("Unable to find SSH Key: %d.", id) } else { id = key.ID } } else { key, _, err := client.Keys.GetByFingerprint(fingerprint) if err != nil { log.Fatalf("Unable to find SSH Key: %q.", fingerprint) } else { id = key.ID } } _, err := client.Keys.DeleteByID(id) if err != nil { log.Fatalf("Unable to destroy SSH Key: %s.", err) } log.Printf("Key %d, %q destroyed.", key.ID, key.Name) }
func domainRecordCreate(ctx *cli.Context) { if len(ctx.Args()) != 1 { cli.ShowAppHelp(ctx) log.Print("Must specify a domain name to add a record to.") os.Exit(1) } domainName := ctx.Args().First() tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) createRequest := &godo.DomainRecordEditRequest{ Type: strings.ToUpper(ctx.String("type")), Name: ctx.String("name"), Data: ctx.String("data"), } if createRequest.Type == "MX" || createRequest.Type == "SRV" { createRequest.Priority = ctx.Int("priority") } if createRequest.Type == "SRV" { createRequest.Port = ctx.Int("port") createRequest.Weight = ctx.Int("weight") } domainRecord, _, err := client.Domains.CreateRecord(domainName, createRequest) if err != nil { log.Fatal(err) } WriteOutput(domainRecord) }
func dropletActionKernels(ctx *cli.Context) { if ctx.Int("id") == 0 && len(ctx.Args()) != 1 { log.Fatal("Error: Must provide ID or name for Droplet to list available kernels.") } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) id := ctx.Int("id") if id == 0 { droplet, err := FindDropletByName(client, ctx.Args()[0]) if err != nil { log.Fatal(err) } else { id = droplet.ID } } droplet, _, err := client.Droplets.Get(id) if err != nil { log.Fatal("Unable to find Droplet: %s.", err) } opt := &godo.ListOptions{} kernelList := []godo.Kernel{} for { // TODO make all optional kernelsPage, resp, err := client.Droplets.Kernels(droplet.ID, opt) if err != nil { log.Fatalf("Unable to list Droplet kernels: %s.", err) } // append the current page's droplets to our list for _, k := range kernelsPage { kernelList = append(kernelList, k) } // if we are at the last page, break out the for loop if resp.Links == nil || resp.Links.IsLastPage() { break } page, err := resp.Links.CurrentPage() if err != nil { log.Fatalf("Unable to get pagination: %s.", err) } // set the page we want for the next request opt.Page = page + 1 } cliOut := NewCLIOutput() defer cliOut.Flush() cliOut.Header("ID", "Name", "Version") for _, kernel := range kernelList { cliOut.Writeln("%d\t%s\t%s\n", kernel.ID, kernel.Name, kernel.Version) } }
func dropletCreate(ctx *cli.Context) { if len(ctx.Args()) != 1 { log.Fatal("Error: Must provide name for Droplet.") } tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) // Add domain to end if available. dropletName := ctx.Args().First() if ctx.Bool("add-region") { dropletName = fmt.Sprintf("%s.%s", dropletName, ctx.String("region")) } if ctx.String("domain") != "" { dropletName = fmt.Sprintf("%s.%s", dropletName, ctx.String("domain")) } // Loop through the SSH Keys and add by name. DO API should have handled // this case as well. var sshKeys []godo.DropletCreateSSHKey keyNames := ctx.String("ssh-keys") if keyNames != "" { for _, keyName := range strings.Split(keyNames, ",") { sshKey, err := FindKeyByName(client, keyName) if sshKey != nil && err == nil { sshKeys = append(sshKeys, godo.DropletCreateSSHKey{ID: sshKey.ID}) } else { log.Fatalf("Warning: Could not find key: %q.", keyName) } } } userData := "" userDataPath := ctx.String("user-data-file") if userDataPath != "" { file, err := os.Open(userDataPath) if err != nil { log.Fatalf("Error opening user data file: %s.", err) } userDataFile, err := ioutil.ReadAll(file) if err != nil { log.Fatalf("Error reading user data file: %s.", err) } userData = string(userDataFile) } else { userData = ctx.String("user-data") } createRequest := &godo.DropletCreateRequest{ Name: dropletName, Region: ctx.String("region"), Size: ctx.String("size"), Image: godo.DropletCreateImage{ Slug: ctx.String("image"), }, SSHKeys: sshKeys, Backups: ctx.Bool("backups"), IPv6: ctx.Bool("ipv6"), PrivateNetworking: ctx.Bool("private-networking"), UserData: userData, } droplet, resp, err := client.Droplets.Create(createRequest) if err != nil { log.Fatalf("Unable to create Droplet: %s.", err) } if ctx.Bool("wait-for-active") { util.WaitForActive(client, resp.Links.Actions[0].HREF) } WriteOutput(droplet) }
func connect(ctx *cli.Context) { if len(ctx.Args()) != 1 { log.Fatal("Error: Must provide name of droplet.") } name := ctx.Args().First() tokenSource := &TokenSource{ AccessToken: APIKey, } oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) client := godo.NewClient(oauthClient) droplet, err := FindDropletByName(client, name) if err != nil { log.Fatal(err) } var ip string for _, n := range droplet.Networks.V4 { if n.Type == "public" { ip = n.IPAddress } } endpoint := fmt.Sprintf("%s:%d", ip, 22) methods, err := authMethods(ctx) if err != nil { log.Fatal(err) } user := "******" if strings.Contains(droplet.Image.Slug, "coreos") { user = "******" } c := &ssh.ClientConfig{ User: user, Auth: methods, } conn, err := ssh.Dial("tcp", endpoint, c) if err != nil { log.Fatal("Unable to connect.", err.Error()) } session, err := conn.NewSession() if err != nil { log.Fatal(err.Error()) } session.Stdout = os.Stdout session.Stderr = os.Stderr session.Stdin = os.Stdin defer session.Close() modes := ssh.TerminalModes{ ssh.ECHO: 1, } fd := os.Stdin.Fd() var ( termWidth, termHeight int ) if term.IsTerminal(fd) { oldState, err := term.MakeRaw(fd) if err != nil { log.Fatal(err) } defer term.RestoreTerminal(fd, oldState) winsize, err := term.GetWinsize(fd) if err != nil { termWidth = 80 termHeight = 24 } else { termWidth = int(winsize.Width) termHeight = int(winsize.Height) } } if err := session.RequestPty("xterm", termWidth, termHeight, modes); err != nil { session.Close() log.Fatalf("request for pseudo terminal failed: %s", err) } if err == nil { err = session.Shell() } if err != nil { log.Fatal(err) } err = session.Wait() if err != nil && err != io.EOF { // Ignore the error if it's an ExitError with an empty message, // this occurs when you do CTRL+c and then run exit cmd which isn't an // actual error. waitMsg, ok := err.(*ssh.ExitError) if ok && waitMsg.Msg() == "" { return } log.Fatal(err) } }