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

Migrate to cobra #86

Merged
merged 10 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Mirgation to urfave/cli
  • Loading branch information
cmoog committed Aug 4, 2020
commit b957f37e5243259bb4fdd5a7c467ea38504fe390
19 changes: 11 additions & 8 deletions ci/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,16 @@ 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)
Expand All @@ -53,6 +52,10 @@ func TestCoderCLI(t *testing.T) {
tcli.Success(),
)

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

c.Run(ctx, "coder urls").Assert(t,
tcli.Error(),
)
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
43 changes: 23 additions & 20 deletions cmd/coder/envs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,30 @@ package main
import (
"fmt"

"github.com/spf13/pflag"

"go.coder.com/cli"
"github.com/urfave/cli"
)

type envsCmd struct {
}

func (cmd envsCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "envs",
Desc: "get a list of environments owned by the authenticated user",
}
}

func (cmd envsCmd) Run(fl *pflag.FlagSet) {
entClient := requireAuth()

envs := getEnvs(entClient)

for _, env := range envs {
fmt.Println(env.Name)
func makeEnvsCommand() cli.Command {
return cli.Command{
Name: "envs",
Usage: "Interact with Coder environments",
Description: "Perform operations on the Coder environments owned by the active user.",
Subcommands: []cli.Command{
{
Name: "ls",
Usage: "list all environments owned by the active user",
Description: "List all Coder environments owned by the active user.",
ArgsUsage: "[...flags]>",
Action: func(c *cli.Context) {
entClient := requireAuth()
envs := getEnvs(entClient)

for _, env := range envs {
fmt.Println(env.Name)
}
},
Flags: nil,
},
},
}
}
Loading