Expand Up @@ -11,162 +11,165 @@ import ( "strings" "time" "github.com/spf13/pflag" "go.coder.com/cli" "go.coder.com/flog" "cdr.dev/coder-cli/internal/config" "cdr.dev/coder-cli/internal/entclient" ) "github.com/urfave/cli" var ( privateKeyFilepath = filepath.Join(os.Getenv("HOME"), ".ssh", "coder_enterprise") "go.coder.com/flog" ) type configSSHCmd struct { filepath string remove bool startToken, startMessage, endToken string } func (cmd *configSSHCmd) Spec() cli.CommandSpec { return cli.CommandSpec{ Name: "config-ssh", Usage: "", Desc: "add your Coder Enterprise environments to ~/.ssh/config", func makeConfigSSHCmd() cli.Command { var ( configpath string remove = false ) return cli.Command{ Name: "config-ssh", UsageText: "", Description: "add your Coder Enterprise environments to ~/.ssh/config", Action: configSSH(&configpath, &remove), Flags: []cli.Flag{ cli.StringFlag{ Name: "filepath", Usage: "overide the default path of your ssh config file", Value: filepath.Join(os.Getenv("HOME"), ".ssh", "config"), TakesFile: true, Destination: &configpath, }, cli.BoolFlag{ Name: "remove", Usage: "remove the auto-generated Coder Enterprise ssh config", Destination: &remove, }, }, } } func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) { fl.BoolVar(&cmd.remove, "remove", false, "remove the auto-generated Coder Enterprise 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-----------" cmd.startMessage = `# The following has been auto-generated by "coder config-ssh" func configSSH(filepath *string, remove *bool) func(c *cli.Context) { startToken := "# ------------START-CODER-ENTERPRISE-----------" 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) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() endToken := "# ------------END-CODER-ENTERPRISE------------" return func(c *cli.Context) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() currentConfig, err := readStr(*filepath) if os.IsNotExist(err) { // SSH configs are not always already there. currentConfig = "" } else if err != nil { flog.Fatal("failed to read ssh config file %q: %v", filepath, err) } currentConfig, err := readStr(cmd.filepath) if os.IsNotExist(err) { // SSH configs are not always already there. currentConfig = "" } else if err != nil { flog.Fatal("failed to read ssh config file %q: %v", cmd.filepath, err) } startIndex := strings.Index(currentConfig, startToken) endIndex := strings.Index(currentConfig, endToken) startIndex := strings.Index(currentConfig, cmd.startToken) endIndex := strings.Index(currentConfig, cmd.endToken) if *remove { if startIndex == -1 || endIndex == -1 { flog.Fatal("the Coder Enterprise ssh configuration section could not be safely deleted or does not exist") } currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(endToken)+1:] 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") } currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(cmd.endToken)+1:] err = writeStr(*filepath, currentConfig) if err != nil { flog.Fatal("failed to write to ssh config file %q: %v", *filepath, err) } err = writeStr(cmd.filepath, currentConfig) if err != nil { flog.Fatal("failed to write to ssh config file %q: %v", cmd.filepath, err) return } return } entClient := requireAuth() entClient := requireAuth() sshAvailable := isSSHAvailable(ctx) if !sshAvailable { flog.Fatal("SSH is disabled or not available for your Coder Enterprise deployment.") } sshAvailable :=cmd.ensureSSHAvailable(ctx )if!sshAvailable { flog.Fatal("SSH is disabled or not available for your Coder Enterprise deployment." ) } me, err :=entClient.Me( ) iferr != nil { flog.Fatal("failed to fetch username: %v", err ) }me, err := entClient.Me() if err != nil { flog.Fatal("failed to fetch username: %v", err) } envs := getEnvs(entClient) if len(envs) < 1 { flog.Fatal("no environments found") } newConfig, err := makeNewConfigs(me.Username, envs, startToken, startMessage, endToken) if err != nil { flog.Fatal("failed to make new ssh configurations: %v", err) } envs := getEnvs(entClient) if len(envs) < 1 { flog.Fatal("no environments found") } 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 { currentConfig = currentConfig[:startIndex-1] + currentConfig[endIndex+len(endToken)+1:] } // 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:] } err = writeStr(*filepath, currentConfig+newConfig) if err != nil { flog.Fatal("failed to write new configurations to ssh config file %q: %v", filepath, err) } err = writeSSHKey(ctx, entClient) if err != nil { flog.Fatal("failed to fetch and write ssh key: %v", err) } 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) } 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", filepath) 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) } 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", 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) } var ( privateKeyFilepath = filepath.Join(os.Getenv("HOME"), ".ssh", "coder_enterprise") ) func writeSSHKey(ctx context.Context, client *entclient.Client) error { key, err := client.SSHKey() if err != nil { return err } err = ioutil.WriteFile(privateKeyFilepath, []byte(key.PrivateKey), 0400) if err != nil { return err } return nil return ioutil.WriteFile(privateKeyFilepath, []byte(key.PrivateKey), 0400) } func(cmd *configSSHCmd) makeNewConfigs(userName string, envs []entclient.Environment) (string, error) { func makeNewConfigs(userName string, envs []entclient.Environment, startToken, startMsg, endToken string ) (string, error) { hostname, err := configuredHostname() if err != nil { return "", nil } newConfig := fmt.Sprintf("\n%s\n%s\n\n",cmd. startToken,cmd.startMessage ) newConfig := fmt.Sprintf("\n%s\n%s\n\n", startToken,startMsg ) for _, env := range envs { newConfig +=cmd.makeConfig (hostname, userName, env.Name) newConfig +=makeSSHConfig (hostname, userName, env.Name) } newConfig += fmt.Sprintf("\n%s\n",cmd. endToken) newConfig += fmt.Sprintf("\n%s\n", endToken) return newConfig, nil } func(cmd *configSSHCmd) makeConfig (host, userName, envName string) string { funcmakeSSHConfig (host, userName, envName string) string { return fmt.Sprintf( `Host coder.%s HostName %s User %s-%s StrictHostKeyChecking no ConnectTimeout=0 IdentityFile=%s ServerAliveInterval 60 ServerAliveCountMax 3 HostName %s User %s-%s StrictHostKeyChecking no ConnectTimeout=0 IdentityFile=%s ServerAliveInterval 60 ServerAliveCountMax 3 `, envName, host, userName, envName, privateKeyFilepath) } func(cmd *configSSHCmd) ensureSSHAvailable (ctx context.Context) bool { funcisSSHAvailable (ctx context.Context) bool { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() Expand Down