From 4c4540a0f2181c8684bfd88248c25e30c4afec95 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 12 May 2020 23:36:55 +0000 Subject: [PATCH 1/6] Add config-ssh command --- cmd/coder/config_ssh.go | 130 +++++++++++++++++++++++++++++++++++++++ cmd/coder/main.go | 6 +- internal/entclient/me.go | 5 +- 3 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 cmd/coder/config_ssh.go diff --git a/cmd/coder/config_ssh.go b/cmd/coder/config_ssh.go new file mode 100644 index 00000000..4b5977fa --- /dev/null +++ b/cmd/coder/config_ssh.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "cdr.dev/coder-cli/internal/entclient" + "github.com/spf13/pflag" + + "go.coder.com/cli" + "go.coder.com/flog" +) + +const ( + sshConfigStartToken = "# ------------START-CODER-ENTERPRISE-----------" + sshConfigStartMessage = `# The following has been auto-generated by "coder config-ssh" +# to make accessing your Coder Enterprise environments easier. +# +# To remove this blob, run: +# +# coder config-ssh --remove +# +# You should not hand-edit this section, unless you are deleting it.` + sshConfigEndToken = "# ------------END-CODER-ENTERPRISE------------" +) + +type configSSHCmd struct { + filepath string + remove bool +} + +func (cmd *configSSHCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "config-ssh", + Usage: "", + Desc: "adds your Coder Enterprise environments to ~/.ssh/config", + } +} + +func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) { + fl.BoolVar(&cmd.remove, "remove", false, "remove the auto-generated Coder Enterprise ssh config") + defaultPath := filepath.Join(os.Getenv("HOME"), ".ssh", "config") + fl.StringVar(&cmd.filepath, "config-path", defaultPath, "overide the default path of your ssh config file") +} + +func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { + currentConfiguration, err := readStr(cmd.filepath) + if err != nil { + flog.Fatal("failed to read ssh config file %q: %v", cmd.filepath, err) + } + + startIndex := strings.Index(currentConfiguration, sshConfigStartToken) + endIndex := strings.Index(currentConfiguration, sshConfigEndToken) + + if cmd.remove { + if startIndex == -1 || endIndex == -1 { + flog.Fatal("the Coder Enterprise ssh configuration section could not be safely deleted or does not exist.") + } + currentConfiguration = currentConfiguration[:startIndex-1] + currentConfiguration[endIndex+len(sshConfigEndToken)+1:] + + err = writeStr(cmd.filepath, currentConfiguration) + if err != nil { + flog.Fatal("failed to write to ssh config file %q: %v", cmd.filepath, err) + } + + return + } + + entClient := requireAuth() + + me, err := entClient.Me() + if err != nil { + flog.Fatal("failed to fetch username: %v", err) + } + envs := getEnvs(entClient) + + if startIndex == -1 || endIndex == -1 { + newConfiguration := makeNewConfigs(me.Username, envs) + + err = writeStr(cmd.filepath, currentConfiguration+newConfiguration) + if err != nil { + flog.Fatal("failed to write new configurations to ssh config file %q: %v", cmd.filepath, err) + } + + return + } + currentConfiguration = currentConfiguration[:startIndex-1] + currentConfiguration[endIndex+len(sshConfigEndToken)+1:] + newConfiguration := makeNewConfigs(me.Username, envs) + + err = writeStr(cmd.filepath, currentConfiguration+newConfiguration) + if err != nil { + flog.Fatal("failed to write new configurations to ssh config file %q: %v", cmd.filepath, err) + } +} + +func makeNewConfigs(userName string, envs []entclient.Environment) string { + newConfiguration := fmt.Sprintf("\n%s\n%s\n\n", sshConfigStartToken, sshConfigStartMessage) + for _, env := range envs { + newConfiguration += makeConfig(userName, env.Name) + } + newConfiguration += fmt.Sprintf("\n%s\n", sshConfigEndToken) + + return newConfiguration +} + +func makeConfig(userName, envName string) string { + return fmt.Sprintf( + `Host coder:%s + HostName %s + User %s-%s + Port 2222 + KeepAlive=yes + ConnectTimeout=0 +`, envName, "MOCK-SSHPROXY-IP", userName, envName) // TODO: get real ssh proxy ip address +} + +func writeStr(filename, data string) error { + return ioutil.WriteFile(filename, []byte(data), 0777) +} + +func readStr(filename string) (string, error) { + contents, err := ioutil.ReadFile(filename) + if err != nil { + return "", err + } + return string(contents), nil +} diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 6f34d152..0aeaf098 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -1,12 +1,13 @@ package main import ( - "github.com/spf13/pflag" - "go.coder.com/cli" "log" "net/http" _ "net/http/pprof" "os" + + "github.com/spf13/pflag" + "go.coder.com/cli" ) var ( @@ -36,6 +37,7 @@ func (r *rootCmd) Subcommands() []cli.Command { &syncCmd{}, &urlCmd{}, &versionCmd{}, + &configSSHCmd{}, } } diff --git a/internal/entclient/me.go b/internal/entclient/me.go index 12fb6940..054ef995 100644 --- a/internal/entclient/me.go +++ b/internal/entclient/me.go @@ -1,8 +1,9 @@ package entclient type User struct { - ID string `json:"id"` - Email string `json:"email"` + ID string `json:"id"` + Email string `json:"email"` + Username string `json:"username"` } func (c Client) Me() (*User, error) { From 50f9ab7ea82710766b9b487d2deb52e4499dfa7d Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Wed, 13 May 2020 04:09:51 +0000 Subject: [PATCH 2/6] Refactor and add success log message --- cmd/coder/config_ssh.go | 72 ++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/cmd/coder/config_ssh.go b/cmd/coder/config_ssh.go index 4b5977fa..7e23fa4c 100644 --- a/cmd/coder/config_ssh.go +++ b/cmd/coder/config_ssh.go @@ -14,22 +14,11 @@ import ( "go.coder.com/flog" ) -const ( - sshConfigStartToken = "# ------------START-CODER-ENTERPRISE-----------" - sshConfigStartMessage = `# The following has been auto-generated by "coder config-ssh" -# to make accessing your Coder Enterprise environments easier. -# -# To remove this blob, run: -# -# coder config-ssh --remove -# -# You should not hand-edit this section, unless you are deleting it.` - sshConfigEndToken = "# ------------END-CODER-ENTERPRISE------------" -) - type configSSHCmd struct { filepath string remove bool + + startToken, startMessage, endToken string } func (cmd *configSSHCmd) Spec() cli.CommandSpec { @@ -44,24 +33,35 @@ func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) { fl.BoolVar(&cmd.remove, "remove", false, "remove the auto-generated Coder Enterprise ssh config") defaultPath := filepath.Join(os.Getenv("HOME"), ".ssh", "config") fl.StringVar(&cmd.filepath, "config-path", defaultPath, "overide the default path of your ssh config file") + + cmd.startToken = "# ------------START-CODER-ENTERPRISE-----------" + cmd.startMessage = `# The following has been auto-generated by "coder config-ssh" +# to make accessing your Coder Enterprise environments easier. +# +# To remove this blob, run: +# +# coder config-ssh --remove +# +# You should not hand-edit this section, unless you are deleting it.` + cmd.endToken = "# ------------END-CODER-ENTERPRISE------------" } func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { - currentConfiguration, err := readStr(cmd.filepath) + currentConfig, err := readStr(cmd.filepath) if err != nil { flog.Fatal("failed to read ssh config file %q: %v", cmd.filepath, err) } - startIndex := strings.Index(currentConfiguration, sshConfigStartToken) - endIndex := strings.Index(currentConfiguration, sshConfigEndToken) + startIndex := strings.Index(currentConfig, cmd.startToken) + endIndex := strings.Index(currentConfig, cmd.endToken) if cmd.remove { if startIndex == -1 || endIndex == -1 { - flog.Fatal("the Coder Enterprise ssh configuration section could not be safely deleted or does not exist.") + flog.Fatal("the Coder Enterprise ssh configuration section could not be safely deleted or does not exist") } - currentConfiguration = currentConfiguration[:startIndex-1] + currentConfiguration[endIndex+len(sshConfigEndToken)+1:] + currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(cmd.endToken)+1:] - err = writeStr(cmd.filepath, currentConfiguration) + err = writeStr(cmd.filepath, currentConfig) if err != nil { flog.Fatal("failed to write to ssh config file %q: %v", cmd.filepath, err) } @@ -75,35 +75,35 @@ func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { if err != nil { flog.Fatal("failed to fetch username: %v", err) } - envs := getEnvs(entClient) - if startIndex == -1 || endIndex == -1 { - newConfiguration := makeNewConfigs(me.Username, envs) - - err = writeStr(cmd.filepath, currentConfiguration+newConfiguration) - if err != nil { - flog.Fatal("failed to write new configurations to ssh config file %q: %v", cmd.filepath, err) - } + envs := getEnvs(entClient) + if len(envs) < 1 { + flog.Fatal("no environments found") + } + newConfig := cmd.makeNewConfigs(me.Username, envs) - return + // if we find the old config, remove those chars from the string + if startIndex != -1 && endIndex != -1 { + currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(cmd.endToken)+1:] } - currentConfiguration = currentConfiguration[:startIndex-1] + currentConfiguration[endIndex+len(sshConfigEndToken)+1:] - newConfiguration := makeNewConfigs(me.Username, envs) - err = writeStr(cmd.filepath, currentConfiguration+newConfiguration) + err = writeStr(cmd.filepath, currentConfig+newConfig) if err != nil { flog.Fatal("failed to write new configurations to ssh config file %q: %v", cmd.filepath, err) } + fmt.Printf("An auto-generated ssh config was written to %q\n", cmd.filepath) + fmt.Println("You should now be able to ssh into your environment") + fmt.Printf("For example, try running\n\n\t$ ssh coder:%s\n\n", envs[0].Name) } -func makeNewConfigs(userName string, envs []entclient.Environment) string { - newConfiguration := fmt.Sprintf("\n%s\n%s\n\n", sshConfigStartToken, sshConfigStartMessage) +func (cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Environment) string { + newConfig := fmt.Sprintf("\n%s\n%s\n\n", cmd.startToken, cmd.startMessage) for _, env := range envs { - newConfiguration += makeConfig(userName, env.Name) + newConfig += makeConfig(userName, env.Name) } - newConfiguration += fmt.Sprintf("\n%s\n", sshConfigEndToken) + newConfig += fmt.Sprintf("\n%s\n", cmd.endToken) - return newConfiguration + return newConfig } func makeConfig(userName, envName string) string { From f9c09300f33e81d31f2502aa795677a8c4e64ad3 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 15 May 2020 17:06:53 +0000 Subject: [PATCH 3/6] Fetch and write private ssh key --- cmd/coder/config_ssh.go | 37 +++++++++++++++++++++++++++++++------ internal/entclient/me.go | 14 ++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/cmd/coder/config_ssh.go b/cmd/coder/config_ssh.go index 7e23fa4c..d0f9f064 100644 --- a/cmd/coder/config_ssh.go +++ b/cmd/coder/config_ssh.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io/ioutil" "os" @@ -19,6 +20,7 @@ type configSSHCmd struct { remove bool startToken, startMessage, endToken string + privateKeyFilepath string } func (cmd *configSSHCmd) Spec() cli.CommandSpec { @@ -31,7 +33,8 @@ func (cmd *configSSHCmd) Spec() cli.CommandSpec { func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) { fl.BoolVar(&cmd.remove, "remove", false, "remove the auto-generated Coder Enterprise ssh config") - defaultPath := filepath.Join(os.Getenv("HOME"), ".ssh", "config") + home := os.Getenv("HOME") + defaultPath := filepath.Join(home, ".ssh", "config") fl.StringVar(&cmd.filepath, "config-path", defaultPath, "overide the default path of your ssh config file") cmd.startToken = "# ------------START-CODER-ENTERPRISE-----------" @@ -44,9 +47,13 @@ func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) { # # You should not hand-edit this section, unless you are deleting it.` cmd.endToken = "# ------------END-CODER-ENTERPRISE------------" + cmd.privateKeyFilepath = filepath.Join(home, ".ssh", "coder_enterprise") } func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + currentConfig, err := readStr(cmd.filepath) if err != nil { flog.Fatal("failed to read ssh config file %q: %v", cmd.filepath, err) @@ -91,30 +98,48 @@ func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { if err != nil { flog.Fatal("failed to write new configurations to ssh config file %q: %v", cmd.filepath, err) } + err = cmd.writeSSHKey(ctx, entClient) + if err != nil { + flog.Fatal("failed to fetch and write ssh key: %v", err) + } + fmt.Printf("An auto-generated ssh config was written to %q\n", cmd.filepath) + fmt.Printf("Your private ssh key was written to %q\n", cmd.privateKeyFilepath) fmt.Println("You should now be able to ssh into your environment") fmt.Printf("For example, try running\n\n\t$ ssh coder:%s\n\n", envs[0].Name) } +func (cmd *configSSHCmd) writeSSHKey(ctx context.Context, client *entclient.Client) error { + key, err := client.SSHKey() + if err != nil { + return err + } + err = ioutil.WriteFile(cmd.privateKeyFilepath, []byte(key.PrivateKey), 400) + if err != nil { + return err + } + return nil +} + func (cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Environment) string { newConfig := fmt.Sprintf("\n%s\n%s\n\n", cmd.startToken, cmd.startMessage) for _, env := range envs { - newConfig += makeConfig(userName, env.Name) + newConfig += cmd.makeConfig(userName, env.Name) } newConfig += fmt.Sprintf("\n%s\n", cmd.endToken) return newConfig } -func makeConfig(userName, envName string) string { +func (cmd *configSSHCmd) makeConfig(userName, envName string) string { return fmt.Sprintf( `Host coder:%s HostName %s User %s-%s - Port 2222 - KeepAlive=yes + StrictHostKeyChecking no ConnectTimeout=0 -`, envName, "MOCK-SSHPROXY-IP", userName, envName) // TODO: get real ssh proxy ip address + IdentityFile=%s +`, envName, "MOCK-SSHPROXY-IP", userName, envName, cmd.privateKeyFilepath) // TODO: get real ssh proxy ip address } func writeStr(filename, data string) error { diff --git a/internal/entclient/me.go b/internal/entclient/me.go index 054ef995..7c7c66f0 100644 --- a/internal/entclient/me.go +++ b/internal/entclient/me.go @@ -14,3 +14,17 @@ func (c Client) Me() (*User, error) { } return &u, nil } + +type SSHKey struct { + PublicKey string `json:"public_key"` + PrivateKey string `json:"private_key"` +} + +func (c Client) SSHKey() (*SSHKey, error) { + var key SSHKey + err := c.requestBody("GET", "/api/users/me/sshkey", nil, &key) + if err != nil { + return nil, err + } + return &key, nil +} From 02c209a44fe8ce2b0389a91b2fe2d5dbdea2eb94 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 19 May 2020 13:54:51 +0000 Subject: [PATCH 4/6] Use coder. to prevent collision with sftp --- cmd/coder/config_ssh.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/coder/config_ssh.go b/cmd/coder/config_ssh.go index d0f9f064..b6470bce 100644 --- a/cmd/coder/config_ssh.go +++ b/cmd/coder/config_ssh.go @@ -106,7 +106,7 @@ func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { fmt.Printf("An auto-generated ssh config was written to %q\n", cmd.filepath) fmt.Printf("Your private ssh key was written to %q\n", cmd.privateKeyFilepath) fmt.Println("You should now be able to ssh into your environment") - fmt.Printf("For example, try running\n\n\t$ ssh coder:%s\n\n", envs[0].Name) + fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n", envs[0].Name) } func (cmd *configSSHCmd) writeSSHKey(ctx context.Context, client *entclient.Client) error { @@ -133,7 +133,7 @@ func (cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Enviro func (cmd *configSSHCmd) makeConfig(userName, envName string) string { return fmt.Sprintf( - `Host coder:%s + `Host coder.%s HostName %s User %s-%s StrictHostKeyChecking no From fcd16c620fb84f1e7bccf69b72a2de87abf2e34d Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 19 May 2020 17:41:24 +0000 Subject: [PATCH 5/6] Use real hostname --- cmd/coder/config_ssh.go | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/cmd/coder/config_ssh.go b/cmd/coder/config_ssh.go index b6470bce..aa301e3d 100644 --- a/cmd/coder/config_ssh.go +++ b/cmd/coder/config_ssh.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "io/ioutil" + "net/url" "os" "path/filepath" "strings" + "cdr.dev/coder-cli/internal/config" "cdr.dev/coder-cli/internal/entclient" "github.com/spf13/pflag" @@ -15,12 +17,15 @@ import ( "go.coder.com/flog" ) +var ( + privateKeyFilepath = filepath.Join(os.Getenv("HOME"), ".ssh", "coder_enterprise") +) + type configSSHCmd struct { filepath string remove bool startToken, startMessage, endToken string - privateKeyFilepath string } func (cmd *configSSHCmd) Spec() cli.CommandSpec { @@ -47,7 +52,6 @@ func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) { # # You should not hand-edit this section, unless you are deleting it.` cmd.endToken = "# ------------END-CODER-ENTERPRISE------------" - cmd.privateKeyFilepath = filepath.Join(home, ".ssh", "coder_enterprise") } func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { @@ -87,7 +91,10 @@ func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { if len(envs) < 1 { flog.Fatal("no environments found") } - newConfig := cmd.makeNewConfigs(me.Username, envs) + newConfig, err := cmd.makeNewConfigs(me.Username, envs) + if err != nil { + flog.Fatal("failed to make new ssh configurations: %v", err) + } // if we find the old config, remove those chars from the string if startIndex != -1 && endIndex != -1 { @@ -98,40 +105,49 @@ func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { if err != nil { flog.Fatal("failed to write new configurations to ssh config file %q: %v", cmd.filepath, err) } - err = cmd.writeSSHKey(ctx, entClient) + err = writeSSHKey(ctx, entClient) if err != nil { flog.Fatal("failed to fetch and write ssh key: %v", err) } fmt.Printf("An auto-generated ssh config was written to %q\n", cmd.filepath) - fmt.Printf("Your private ssh key was written to %q\n", cmd.privateKeyFilepath) + fmt.Printf("Your private ssh key was written to %q\n", privateKeyFilepath) fmt.Println("You should now be able to ssh into your environment") fmt.Printf("For example, try running\n\n\t$ ssh coder.%s\n\n", envs[0].Name) } -func (cmd *configSSHCmd) writeSSHKey(ctx context.Context, client *entclient.Client) error { +func writeSSHKey(ctx context.Context, client *entclient.Client) error { key, err := client.SSHKey() if err != nil { return err } - err = ioutil.WriteFile(cmd.privateKeyFilepath, []byte(key.PrivateKey), 400) + err = ioutil.WriteFile(privateKeyFilepath, []byte(key.PrivateKey), 400) if err != nil { return err } return nil } -func (cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Environment) string { +func (cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Environment) (string, error) { + u, err := config.URL.Read() + if err != nil { + return "", err + } + url, err := url.Parse(u) + if err != nil { + return "", err + } + newConfig := fmt.Sprintf("\n%s\n%s\n\n", cmd.startToken, cmd.startMessage) for _, env := range envs { - newConfig += cmd.makeConfig(userName, env.Name) + newConfig += cmd.makeConfig(url.Hostname(), userName, env.Name) } newConfig += fmt.Sprintf("\n%s\n", cmd.endToken) - return newConfig + return newConfig, nil } -func (cmd *configSSHCmd) makeConfig(userName, envName string) string { +func (cmd *configSSHCmd) makeConfig(host, userName, envName string) string { return fmt.Sprintf( `Host coder.%s HostName %s @@ -139,7 +155,7 @@ func (cmd *configSSHCmd) makeConfig(userName, envName string) string { StrictHostKeyChecking no ConnectTimeout=0 IdentityFile=%s -`, envName, "MOCK-SSHPROXY-IP", userName, envName, cmd.privateKeyFilepath) // TODO: get real ssh proxy ip address +`, envName, host, userName, envName, privateKeyFilepath) } func writeStr(filename, data string) error { From ce4bb7d7298f99619401e889a293b719bd758762 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Tue, 26 May 2020 19:27:04 +0000 Subject: [PATCH 6/6] Check if ssh is available --- cmd/coder/config_ssh.go | 45 +++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/cmd/coder/config_ssh.go b/cmd/coder/config_ssh.go index aa301e3d..d5bf10ea 100644 --- a/cmd/coder/config_ssh.go +++ b/cmd/coder/config_ssh.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "io/ioutil" + "net" "net/url" "os" "path/filepath" "strings" + "time" "cdr.dev/coder-cli/internal/config" "cdr.dev/coder-cli/internal/entclient" @@ -32,7 +34,7 @@ func (cmd *configSSHCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ Name: "config-ssh", Usage: "", - Desc: "adds your Coder Enterprise environments to ~/.ssh/config", + Desc: "add your Coder Enterprise environments to ~/.ssh/config", } } @@ -82,6 +84,11 @@ func (cmd *configSSHCmd) Run(fl *pflag.FlagSet) { entClient := requireAuth() + sshAvailable := cmd.ensureSSHAvailable(ctx) + if !sshAvailable { + flog.Fatal("SSH is disabled or not available for your Coder Enterprise deployment.") + } + me, err := entClient.Me() if err != nil { flog.Fatal("failed to fetch username: %v", err) @@ -129,18 +136,14 @@ func writeSSHKey(ctx context.Context, client *entclient.Client) error { } func (cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Environment) (string, error) { - u, err := config.URL.Read() + hostname, err := configuredHostname() if err != nil { - return "", err - } - url, err := url.Parse(u) - if err != nil { - return "", err + return "", nil } newConfig := fmt.Sprintf("\n%s\n%s\n\n", cmd.startToken, cmd.startMessage) for _, env := range envs { - newConfig += cmd.makeConfig(url.Hostname(), userName, env.Name) + newConfig += cmd.makeConfig(hostname, userName, env.Name) } newConfig += fmt.Sprintf("\n%s\n", cmd.endToken) @@ -158,6 +161,32 @@ func (cmd *configSSHCmd) makeConfig(host, userName, envName string) string { `, envName, host, userName, envName, privateKeyFilepath) } +func (cmd *configSSHCmd) ensureSSHAvailable(ctx context.Context) bool { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + host, err := configuredHostname() + if err != nil { + return false + } + + var dialer net.Dialer + _, err = dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, "22")) + return err == nil +} + +func configuredHostname() (string, error) { + u, err := config.URL.Read() + if err != nil { + return "", err + } + url, err := url.Parse(u) + if err != nil { + return "", err + } + return url.Hostname(), nil +} + func writeStr(filename, data string) error { return ioutil.WriteFile(filename, []byte(data), 0777) }