Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Environment subcommands #89

Closed
wants to merge 14 commits into from
19 changes: 11 additions & 8 deletions ci/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,25 @@ func TestCoderCLI(t *testing.T) {
tcli.StderrEmpty(),
)

c.Run(ctx, "coder version").Assert(t,
c.Run(ctx, "coder --version").Assert(t,
tcli.StderrEmpty(),
tcli.Success(),
tcli.StdoutMatches("linux"),
)

c.Run(ctx, "coder help").Assert(t,
c.Run(ctx, "coder --help").Assert(t,
tcli.Success(),
tcli.StderrMatches("Commands:"),
tcli.StderrMatches("Usage: coder"),
tcli.StdoutEmpty(),
tcli.StdoutMatches("COMMANDS:"),
tcli.StdoutMatches("USAGE:"),
)

headlessLogin(ctx, t, c)

c.Run(ctx, "coder envs").Assert(t,
tcli.Error(),
)

c.Run(ctx, "coder envs ls").Assert(t,
tcli.Success(),
)

Expand All @@ -66,14 +69,14 @@ func TestCoderCLI(t *testing.T) {
)

var user entclient.User
c.Run(ctx, `coder users ls -o json | jq -c '.[] | select( .username == "charlie")'`).Assert(t,
c.Run(ctx, `coder users ls --output json | jq -c '.[] | select( .username == "charlie")'`).Assert(t,
tcli.Success(),
stdoutUnmarshalsJSON(&user),
)
assert.Equal(t, "user email is as expected", "charlie@coder.com", user.Email)
assert.Equal(t, "username is as expected", "Charlie", user.Name)

c.Run(ctx, "coder users ls -o human | grep charlie").Assert(t,
c.Run(ctx, "coder users ls --output human | grep charlie").Assert(t,
tcli.Success(),
tcli.StdoutMatches("charlie"),
)
Expand All @@ -82,7 +85,7 @@ func TestCoderCLI(t *testing.T) {
tcli.Success(),
)

c.Run(ctx, "coder envs").Assert(t,
c.Run(ctx, "coder envs ls").Assert(t,
tcli.Error(),
)
}
Expand Down
1 change: 0 additions & 1 deletion ci/integration/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ func TestSecrets(t *testing.T) {

c.Run(ctx, "coder secrets create").Assert(t,
tcli.Error(),
tcli.StdoutEmpty(),
)

// this tests the "Value:" prompt fallback
Expand Down
209 changes: 106 additions & 103 deletions cmd/coder/configssh.go
Original file line number Diff line number Diff line change
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",
Usage: "Configure SSH to access Coder environments",
Description: "Inject the proper OpenSSH configuration into your local SSH config file.",
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()
if err != 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 {
func makeSSHConfig(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 {
func isSSHAvailable(ctx context.Context) bool {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

Expand Down
Loading