From ad0f9c96656e09860e76ae2e67313d8a11ffd7e1 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 31 Jul 2020 12:51:11 -0500 Subject: [PATCH 01/14] WIP mirgation to urfave/cli --- cmd/coder/configssh.go | 209 +++++++++++++++++++++-------------------- cmd/coder/login.go | 30 +++--- cmd/coder/logout.go | 19 ++-- cmd/coder/main.go | 39 +++++--- cmd/coder/secrets.go | 8 -- cmd/coder/shell.go | 58 +++++++----- cmd/coder/users.go | 124 ++++++++++++------------ cmd/coder/version.go | 28 ------ go.mod | 1 + go.sum | 8 ++ 10 files changed, 253 insertions(+), 271 deletions(-) delete mode 100644 cmd/coder/version.go diff --git a/cmd/coder/configssh.go b/cmd/coder/configssh.go index ff751322..f6d3b794 100644 --- a/cmd/coder/configssh.go +++ b/cmd/coder/configssh.go @@ -11,41 +11,44 @@ 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: @@ -53,120 +56,120 @@ func (cmd *configSSHCmd) RegisterFlags(fl *pflag.FlagSet) { # 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() diff --git a/cmd/coder/login.go b/cmd/coder/login.go index fef1a38b..70da187e 100644 --- a/cmd/coder/login.go +++ b/cmd/coder/login.go @@ -7,30 +7,28 @@ import ( "strings" "sync" + "cdr.dev/coder-cli/internal/config" + "cdr.dev/coder-cli/internal/loginsrv" "github.com/pkg/browser" - "github.com/spf13/pflag" + "github.com/urfave/cli" - "go.coder.com/cli" "go.coder.com/flog" - - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/internal/loginsrv" ) -type loginCmd struct { -} - -func (cmd loginCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "login", - Usage: "[Coder Enterprise URL eg. http://my.coder.domain/ ]", - Desc: "authenticate this client for future operations", +func makeLoginCmd() cli.Command { + cmd := cli.Command{ + Name: "login", + Usage: "[Coder Enterprise URL eg. http://my.coder.domain/ ]", + Description: "authenticate this client for future operations", + Action: login, } + return cmd } -func (cmd loginCmd) Run(fl *pflag.FlagSet) { - rawURL := fl.Arg(0) + +func login(c *cli.Context) { + rawURL := c.Args().First() if rawURL == "" || !strings.HasPrefix(rawURL, "http") { - exitUsage(fl) + flog.Fatal("invalid URL") } u, err := url.Parse(rawURL) diff --git a/cmd/coder/logout.go b/cmd/coder/logout.go index 6120f527..e90d98f8 100644 --- a/cmd/coder/logout.go +++ b/cmd/coder/logout.go @@ -3,25 +3,22 @@ package main import ( "os" - "github.com/spf13/pflag" + "cdr.dev/coder-cli/internal/config" + "github.com/urfave/cli" - "go.coder.com/cli" "go.coder.com/flog" - - "cdr.dev/coder-cli/internal/config" ) -type logoutCmd struct { -} - -func (cmd logoutCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ +func makeLogoutCmd() cli.Command { + return cli.Command{ Name: "logout", - Desc: "remove local authentication credentials (if any)", + //Usage: "", + Description: "remove local authentication credentials (if any)", + Action: logout, } } -func (cmd logoutCmd) Run(_ *pflag.FlagSet) { +func logout(c *cli.Context) { err := config.Session.Delete() if err != nil { if os.IsNotExist(err) { diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 5680d30d..af2396c5 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -1,48 +1,45 @@ package main import ( + "fmt" "log" "net/http" _ "net/http/pprof" "os" + "runtime" "cdr.dev/coder-cli/internal/x/xterminal" "github.com/spf13/pflag" + "github.com/urfave/cli" + cdrcli "go.coder.com/cli" "go.coder.com/flog" - - "go.coder.com/cli" ) var ( - version string = "No version built" + version string = "unknown" ) type rootCmd struct{} func (r *rootCmd) Run(fl *pflag.FlagSet) { + fl.Usage() } -func (r *rootCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ +func (r *rootCmd) Spec() cdrcli.CommandSpec { + return cdrcli.CommandSpec{ Name: "coder", Usage: "[subcommand] [flags]", Desc: "coder provides a CLI for working with an existing Coder Enterprise installation.", } } -func (r *rootCmd) Subcommands() []cli.Command { - return []cli.Command{ +func (r *rootCmd) Subcommands() []cdrcli.Command { + return []cdrcli.Command{ &envsCmd{}, - &loginCmd{}, - &logoutCmd{}, - &shellCmd{}, &syncCmd{}, &urlsCmd{}, - &versionCmd{}, - &configSSHCmd{}, - &usersCmd{}, &secretsCmd{}, } } @@ -60,7 +57,21 @@ func main() { } defer xterminal.Restore(os.Stdout.Fd(), stdoutState) - cli.RunRoot(&rootCmd{}) + app := cli.NewApp() + app.Name = "coder" + app.Usage = "coder provides a CLI for working with an existing Coder Enterprise installation" + app.Version = fmt.Sprintf("%s %s %s/%s", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) + app.Commands = []cli.Command{ + makeLoginCmd(), + makeLogoutCmd(), + makeShellCmd(), + makeUsersCmd(), + makeConfigSSHCmd(), + } + err = app.Run(os.Args) + if err != nil { + flog.Fatal("%v", err) + } } // requireSuccess prints the given message and format args as a fatal error if err != nil diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go index a07df2de..893af2bc 100644 --- a/cmd/coder/secrets.go +++ b/cmd/coder/secrets.go @@ -17,14 +17,6 @@ import ( "go.coder.com/cli" ) -var ( - _ cli.FlaggedCommand = secretsCmd{} - _ cli.ParentCommand = secretsCmd{} - - _ cli.FlaggedCommand = &listSecretsCmd{} - _ cli.FlaggedCommand = &createSecretCmd{} -) - type secretsCmd struct { } diff --git a/cmd/coder/shell.go b/cmd/coder/shell.go index c7b42564..15d09167 100644 --- a/cmd/coder/shell.go +++ b/cmd/coder/shell.go @@ -7,48 +7,56 @@ import ( "strings" "time" - "github.com/spf13/pflag" + "cdr.dev/coder-cli/internal/activity" + "cdr.dev/coder-cli/internal/x/xterminal" + "cdr.dev/wsep" + "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" "golang.org/x/time/rate" "golang.org/x/xerrors" "nhooyr.io/websocket" - "go.coder.com/cli" "go.coder.com/flog" - - "cdr.dev/coder-cli/internal/activity" - "cdr.dev/coder-cli/internal/x/xterminal" - "cdr.dev/wsep" ) -type shellCmd struct{} - -func (cmd *shellCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "sh", - Usage: " []", - Desc: "execute a remote command on the environment\nIf no command is specified, the default shell is opened.", - RawArgs: true, +func makeShellCmd() cli.Command { + return cli.Command{ + Name: "sh", + Usage: " []", + Description: "execute a remote command on the environment\\nIf no command is specified, the default shell is opened.", + SkipFlagParsing: true, + SkipArgReorder: true, + + ShortName: "", + Aliases: nil, + UsageText: "", + ArgsUsage: "", + Category: "", + BashComplete: nil, + Before: nil, + After: nil, + Action: shell, + OnUsageError: nil, + Subcommands: nil, + Flags: nil, + HideHelp: false, + Hidden: false, + UseShortOptionHandling: false, + HelpName: "", + CustomHelpTemplate: "", } } -type resizeEvent struct { - height, width uint16 -} - -func (cmd *shellCmd) Run(fl *pflag.FlagSet) { - if len(fl.Args()) < 1 { - exitUsage(fl) - } +func shell(c *cli.Context) { var ( - envName = fl.Arg(0) + envName = c.Args().First() ctx = context.Background() ) command := "sh" args := []string{"-c"} - if len(fl.Args()) > 1 { - args = append(args, strings.Join(fl.Args()[1:], " ")) + if len(c.Args().Tail()) > 0 { + args = append(args, strings.Join(c.Args().Tail(), " ")) } else { // Bring user into shell if no command is specified. args = append(args, "exec $(getent passwd $(whoami) | awk -F: '{ print $7 }')") diff --git a/cmd/coder/users.go b/cmd/coder/users.go index e050edfb..0d1fc031 100644 --- a/cmd/coder/users.go +++ b/cmd/coder/users.go @@ -6,80 +6,72 @@ import ( "os" "cdr.dev/coder-cli/internal/x/xtabwriter" - "cdr.dev/coder-cli/internal/x/xvalidate" - "github.com/spf13/pflag" + "github.com/urfave/cli" - "go.coder.com/cli" + "go.coder.com/flog" ) -type usersCmd struct { -} - -func (cmd usersCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "users", - Usage: "[subcommand] ", - Desc: "interact with user accounts", +func makeUsersCmd() cli.Command { + var output string + return cli.Command{ + Name: "users", + ShortName: "", + Aliases: nil, + Usage: "[subcommand] ", + UsageText: "", + Description: "", + ArgsUsage: "", + Action: nil, + OnUsageError: nil, + Subcommands: []cli.Command{ + { + Name: "ls", + Usage: "", + UsageText: "", + Description: "", + ArgsUsage: "", + Category: "", + Action: listUsers(&output), + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "output", + Usage: "", + Required: false, + Value: "human", + Destination: &output, + }, + }, + }, + }, + HelpName: "", } } -func (cmd usersCmd) Run(fl *pflag.FlagSet) { - exitUsage(fl) -} - -func (cmd *usersCmd) Subcommands() []cli.Command { - return []cli.Command{ - &listCmd{}, - } -} - -type listCmd struct { - outputFmt string -} - -func (cmd *listCmd) Run(fl *pflag.FlagSet) { - xvalidate.Validate(fl, cmd) - entClient := requireAuth() +func listUsers(outputFmt *string) func(c *cli.Context) { + return func(c *cli.Context) { + entClient := requireAuth() - users, err := entClient.Users() - requireSuccess(err, "failed to get users: %v", err) + users, err := entClient.Users() + requireSuccess(err, "failed to get users: %v", err) - switch cmd.outputFmt { - case "human": - w := xtabwriter.NewWriter() - if len(users) > 0 { - _, err = fmt.Fprintln(w, xtabwriter.StructFieldNames(users[0])) - requireSuccess(err, "failed to write: %v", err) + switch *outputFmt { + case "human": + w := xtabwriter.NewWriter() + if len(users) > 0 { + _, err = fmt.Fprintln(w, xtabwriter.StructFieldNames(users[0])) + requireSuccess(err, "failed to write: %v", err) + } + for _, u := range users { + _, err = fmt.Fprintln(w, xtabwriter.StructValues(u)) + requireSuccess(err, "failed to write: %v", err) + } + err = w.Flush() + requireSuccess(err, "failed to flush writer: %v", err) + case "json": + err = json.NewEncoder(os.Stdout).Encode(users) + requireSuccess(err, "failed to encode users to json: %v", err) + default: + flog.Fatal("unknown value for --output") } - for _, u := range users { - _, err = fmt.Fprintln(w, xtabwriter.StructValues(u)) - requireSuccess(err, "failed to write: %v", err) - } - err = w.Flush() - requireSuccess(err, "failed to flush writer: %v", err) - case "json": - err = json.NewEncoder(os.Stdout).Encode(users) - requireSuccess(err, "failed to encode users to json: %v", err) - default: - exitUsage(fl) - } -} - -func (cmd *listCmd) RegisterFlags(fl *pflag.FlagSet) { - fl.StringVarP(&cmd.outputFmt, "output", "o", "human", "output format (human | json)") -} - -func (cmd *listCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "ls", - Usage: "", - Desc: "list all users", - } -} - -func (cmd *listCmd) Validate(fl *pflag.FlagSet) (e []error) { - if !(cmd.outputFmt == "json" || cmd.outputFmt == "human") { - e = append(e, fmt.Errorf(`--output must be "json" or "human"`)) } - return e } diff --git a/cmd/coder/version.go b/cmd/coder/version.go deleted file mode 100644 index a1825843..00000000 --- a/cmd/coder/version.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "fmt" - "runtime" - - "github.com/spf13/pflag" - - "go.coder.com/cli" -) - -type versionCmd struct{} - -func (versionCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "version", - Usage: "", - Desc: "print the currently installed CLI version", - } -} - -func (versionCmd) Run(fl *pflag.FlagSet) { - fmt.Println( - version, - runtime.Version(), - runtime.GOOS+"/"+runtime.GOARCH, - ) -} diff --git a/go.mod b/go.mod index 7318d3f6..0ce94e91 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/pflag v1.0.5 + github.com/urfave/cli v1.22.4 go.coder.com/cli v0.4.0 go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512 golang.org/x/crypto v0.0.0-20200422194213-44a606286825 diff --git a/go.sum b/go.sum index cba75741..3866f35e 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= @@ -165,8 +167,12 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -179,6 +185,8 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= go.coder.com/cli v0.4.0 h1:PruDGwm/CPFndyK/eMowZG3vzg5CgohRWeXWCTr3zi8= From 659aabcfd3df3ecaca7149d192a2c35a652c7919 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 31 Jul 2020 13:39:36 -0500 Subject: [PATCH 02/14] Migrate secrets to urfave --- cmd/coder/main.go | 2 +- cmd/coder/secrets.go | 256 ++++++++++++++++++++----------------------- 2 files changed, 120 insertions(+), 138 deletions(-) diff --git a/cmd/coder/main.go b/cmd/coder/main.go index af2396c5..85802b2d 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -40,7 +40,6 @@ func (r *rootCmd) Subcommands() []cdrcli.Command { &envsCmd{}, &syncCmd{}, &urlsCmd{}, - &secretsCmd{}, } } @@ -67,6 +66,7 @@ func main() { makeShellCmd(), makeUsersCmd(), makeConfigSSHCmd(), + makeSecretsCmd(), } err = app.Run(os.Args) if err != nil { diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go index 893af2bc..8d03f3b1 100644 --- a/cmd/coder/secrets.go +++ b/cmd/coder/secrets.go @@ -7,52 +7,132 @@ import ( "cdr.dev/coder-cli/internal/entclient" "cdr.dev/coder-cli/internal/x/xtabwriter" - "cdr.dev/coder-cli/internal/x/xvalidate" "github.com/manifoldco/promptui" - "github.com/spf13/pflag" + "github.com/urfave/cli" "golang.org/x/xerrors" "go.coder.com/flog" - - "go.coder.com/cli" ) -type secretsCmd struct { -} - -func (cmd secretsCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "secrets", - Usage: "[subcommand]", - Desc: "interact with secrets", - } -} - -func (cmd secretsCmd) Run(fl *pflag.FlagSet) { - exitUsage(fl) -} - -func (cmd secretsCmd) RegisterFlags(fl *pflag.FlagSet) {} - -func (cmd secretsCmd) Subcommands() []cli.Command { - return []cli.Command{ - &listSecretsCmd{}, - &viewSecretsCmd{}, - &createSecretCmd{}, - &deleteSecretsCmd{}, +func makeSecretsCmd() cli.Command { + return cli.Command{ + Name: "secrets", + Usage: "", + Description: "interact with secrets", + Subcommands: []cli.Command{ + { + Name: "ls", + Usage: "", + Description: "", + Action: listSecrets, + }, + makeCreateSecret(), + { + Name: "rm", + Usage: "", + Description: "", + Action: removeSecret, + }, + { + Name: "view", + Usage: "", + Description: "", + Action: viewSecret, + }, + }, + Flags: nil, } } -type listSecretsCmd struct{} +func makeCreateSecret() cli.Command { + var ( + fromFile string + fromLiteral string + fromPrompt bool + description string + ) -func (cmd *listSecretsCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "ls", - Desc: "list all secrets", + return cli.Command{ + Name: "create", + Usage: "", + Description: "", + Before: func(c *cli.Context) error { + if c.Args().First() == "" { + return xerrors.Errorf("[name] is a required field argument") + } + if fromPrompt && (fromLiteral != "" || fromFile != "") { + return xerrors.Errorf("--from-prompt cannot be set along with --from-file or --from-literal") + } + if fromLiteral != "" && fromFile != "" { + return xerrors.Errorf("--from-literal and --from-file cannot both be set") + } + if !fromPrompt && fromFile == "" && fromLiteral == "" { + return xerrors.Errorf("one of [--from-literal, --from-file, --from-prompt] is required") + } + return nil + }, + Action: func(c *cli.Context) { + var ( + client = requireAuth() + name = c.Args().First() + value string + err error + ) + if fromLiteral != "" { + value = fromLiteral + } else if fromFile != "" { + contents, err := ioutil.ReadFile(fromFile) + requireSuccess(err, "failed to read file: %v", err) + value = string(contents) + } else { + prompt := promptui.Prompt{ + Label: "value", + Mask: '*', + Validate: func(s string) error { + if len(s) < 1 { + return xerrors.Errorf("a length > 0 is required") + } + return nil + }, + } + value, err = prompt.Run() + requireSuccess(err, "failed to prompt for value: %v", err) + } + + err = client.InsertSecret(entclient.InsertSecretReq{ + Name: name, + Value: value, + Description: description, + }) + requireSuccess(err, "failed to insert secret: %v", err) + }, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "from-file", + Usage: "a file from which to read the value of the secret", + TakesFile: true, + Destination: &fromFile, + }, + cli.StringFlag{ + Name: "from-literal", + Usage: "the value of the secret", + Destination: &fromLiteral, + }, + cli.BoolFlag{ + Name: "from-prompt", + Usage: "enter the secret value through a terminal prompt", + Destination: &fromPrompt, + }, + cli.StringFlag{ + Name: "description", + Usage: "a description of the secret", + Destination: &description, + }, + }, } } -func (cmd *listSecretsCmd) Run(fl *pflag.FlagSet) { +func listSecrets(_ *cli.Context) { client := requireAuth() secrets, err := client.Secrets() @@ -76,25 +156,13 @@ func (cmd *listSecretsCmd) Run(fl *pflag.FlagSet) { requireSuccess(err, "failed to flush writer: %v", err) } -func (cmd *listSecretsCmd) RegisterFlags(fl *pflag.FlagSet) {} - -type viewSecretsCmd struct{} - -func (cmd viewSecretsCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "view", - Usage: "[secret_name]", - Desc: "view a secret", - } -} - -func (cmd viewSecretsCmd) Run(fl *pflag.FlagSet) { +func viewSecret(c *cli.Context) { var ( client = requireAuth() - name = fl.Arg(0) + name = c.Args().First() ) if name == "" { - exitUsage(fl) + flog.Fatal("[name] is a required argument") } secret, err := client.SecretByName(name) @@ -104,99 +172,13 @@ func (cmd viewSecretsCmd) Run(fl *pflag.FlagSet) { requireSuccess(err, "failed to write: %v", err) } -type createSecretCmd struct { - description string - fromFile string - fromLiteral string - fromPrompt bool -} - -func (cmd *createSecretCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "create", - Usage: `[secret_name] [...flags]`, - Desc: "create a new secret", - } -} - -func (cmd *createSecretCmd) Validate(fl *pflag.FlagSet) (e []error) { - if cmd.fromPrompt && (cmd.fromLiteral != "" || cmd.fromFile != "") { - e = append(e, xerrors.Errorf("--from-prompt cannot be set along with --from-file or --from-literal")) - } - if cmd.fromLiteral != "" && cmd.fromFile != "" { - e = append(e, xerrors.Errorf("--from-literal and --from-file cannot both be set")) - } - if !cmd.fromPrompt && cmd.fromFile == "" && cmd.fromLiteral == "" { - e = append(e, xerrors.Errorf("one of [--from-literal, --from-file, --from-prompt] is required")) - } - return e -} - -func (cmd *createSecretCmd) Run(fl *pflag.FlagSet) { - var ( - client = requireAuth() - name = fl.Arg(0) - value string - err error - ) - if name == "" { - exitUsage(fl) - } - xvalidate.Validate(fl, cmd) - - if cmd.fromLiteral != "" { - value = cmd.fromLiteral - } else if cmd.fromFile != "" { - contents, err := ioutil.ReadFile(cmd.fromFile) - requireSuccess(err, "failed to read file: %v", err) - value = string(contents) - } else { - prompt := promptui.Prompt{ - Label: "value", - Mask: '*', - Validate: func(s string) error { - if len(s) < 1 { - return xerrors.Errorf("a length > 0 is required") - } - return nil - }, - } - value, err = prompt.Run() - requireSuccess(err, "failed to prompt for value: %v", err) - } - - err = client.InsertSecret(entclient.InsertSecretReq{ - Name: name, - Value: value, - Description: cmd.description, - }) - requireSuccess(err, "failed to insert secret: %v", err) -} - -func (cmd *createSecretCmd) RegisterFlags(fl *pflag.FlagSet) { - fl.StringVar(&cmd.fromFile, "from-file", "", "specify a file from which to read the value of the secret") - fl.StringVar(&cmd.fromLiteral, "from-literal", "", "specify the value of the secret") - fl.BoolVar(&cmd.fromPrompt, "from-prompt", false, "specify the value of the secret through a prompt") - fl.StringVar(&cmd.description, "description", "", "specify a description of the secret") -} - -type deleteSecretsCmd struct{} - -func (cmd *deleteSecretsCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "rm", - Usage: "[secret_name]", - Desc: "remove a secret", - } -} - -func (cmd *deleteSecretsCmd) Run(fl *pflag.FlagSet) { +func removeSecret(c *cli.Context) { var ( client = requireAuth() - name = fl.Arg(0) + name = c.Args().First() ) if name == "" { - exitUsage(fl) + flog.Fatal("[name] is a required argument") } err := client.DeleteSecretByName(name) From b2c08eb8faf848257b0abfd13369ba9f61751d12 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 31 Jul 2020 13:54:05 -0500 Subject: [PATCH 03/14] Migrate envs --- cmd/coder/envs.go | 44 ++++++++++++++++++++++++-------------------- cmd/coder/main.go | 2 +- cmd/coder/shell.go | 21 +++------------------ 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/cmd/coder/envs.go b/cmd/coder/envs.go index 9e45df1f..a83afb05 100644 --- a/cmd/coder/envs.go +++ b/cmd/coder/envs.go @@ -3,27 +3,31 @@ 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", + UsageText: "", + Description: "interact with Coder environments", + Subcommands: []cli.Command{ + { + Name: "ls", + Usage: "list all environments owned by the active user", + UsageText: "", + Description: "", + ArgsUsage: "[...flags]>", + Action: func(c *cli.Context) { + entClient := requireAuth() + envs := getEnvs(entClient) + + for _, env := range envs { + fmt.Println(env.Name) + } + }, + Flags: nil, + }, + }, } } diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 85802b2d..ee20fcd6 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -37,7 +37,6 @@ func (r *rootCmd) Spec() cdrcli.CommandSpec { func (r *rootCmd) Subcommands() []cdrcli.Command { return []cdrcli.Command{ - &envsCmd{}, &syncCmd{}, &urlsCmd{}, } @@ -67,6 +66,7 @@ func main() { makeUsersCmd(), makeConfigSSHCmd(), makeSecretsCmd(), + makeEnvsCommand(), } err = app.Run(os.Args) if err != nil { diff --git a/cmd/coder/shell.go b/cmd/coder/shell.go index 15d09167..c40933ea 100644 --- a/cmd/coder/shell.go +++ b/cmd/coder/shell.go @@ -26,24 +26,9 @@ func makeShellCmd() cli.Command { Description: "execute a remote command on the environment\\nIf no command is specified, the default shell is opened.", SkipFlagParsing: true, SkipArgReorder: true, - - ShortName: "", - Aliases: nil, - UsageText: "", - ArgsUsage: "", - Category: "", - BashComplete: nil, - Before: nil, - After: nil, - Action: shell, - OnUsageError: nil, - Subcommands: nil, - Flags: nil, - HideHelp: false, - Hidden: false, - UseShortOptionHandling: false, - HelpName: "", - CustomHelpTemplate: "", + UsageText: "", + ArgsUsage: "", + Action: shell, } } From 8f4a73c155b73354d4c80a6245e381f16d18e580 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 31 Jul 2020 14:18:00 -0500 Subject: [PATCH 04/14] fixup! Migrate envs --- ci/integration/integration_test.go | 2 +- cmd/coder/main.go | 25 +---- cmd/coder/secrets.go | 11 +- cmd/coder/sync.go | 155 +++++++++++++++-------------- internal/x/xvalidate/errors.go | 101 ------------------- 5 files changed, 88 insertions(+), 206 deletions(-) delete mode 100644 internal/x/xvalidate/errors.go diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index dc921f29..098a4791 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -34,7 +34,7 @@ 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"), diff --git a/cmd/coder/main.go b/cmd/coder/main.go index ee20fcd6..8b049d85 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -9,10 +9,8 @@ import ( "runtime" "cdr.dev/coder-cli/internal/x/xterminal" - "github.com/spf13/pflag" "github.com/urfave/cli" - cdrcli "go.coder.com/cli" "go.coder.com/flog" ) @@ -20,28 +18,6 @@ var ( version string = "unknown" ) -type rootCmd struct{} - -func (r *rootCmd) Run(fl *pflag.FlagSet) { - - fl.Usage() -} - -func (r *rootCmd) Spec() cdrcli.CommandSpec { - return cdrcli.CommandSpec{ - Name: "coder", - Usage: "[subcommand] [flags]", - Desc: "coder provides a CLI for working with an existing Coder Enterprise installation.", - } -} - -func (r *rootCmd) Subcommands() []cdrcli.Command { - return []cdrcli.Command{ - &syncCmd{}, - &urlsCmd{}, - } -} - func main() { if os.Getenv("PPROF") != "" { go func() { @@ -67,6 +43,7 @@ func main() { makeConfigSSHCmd(), makeSecretsCmd(), makeEnvsCommand(), + makeSyncCmd(), } err = app.Run(os.Args) if err != nil { diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go index 8d03f3b1..91fa8c4e 100644 --- a/cmd/coder/secrets.go +++ b/cmd/coder/secrets.go @@ -18,12 +18,12 @@ func makeSecretsCmd() cli.Command { return cli.Command{ Name: "secrets", Usage: "", - Description: "interact with secrets", + Description: "Interact with secrets objects owned by the active user.", Subcommands: []cli.Command{ { Name: "ls", Usage: "", - Description: "", + Description: "list all secrets owned by the active user", Action: listSecrets, }, makeCreateSecret(), @@ -54,11 +54,12 @@ func makeCreateSecret() cli.Command { return cli.Command{ Name: "create", - Usage: "", - Description: "", + Usage: "create a new secret", + Description: "Create a new secret object to store application secrets and access them securely from within your environments.", + ArgsUsage: "[secret_name]", Before: func(c *cli.Context) error { if c.Args().First() == "" { - return xerrors.Errorf("[name] is a required field argument") + return xerrors.Errorf("[secret_name] is a required argument") } if fromPrompt && (fromLiteral != "" || fromFile != "") { return xerrors.Errorf("--from-prompt cannot be set along with --from-file or --from-literal") diff --git a/cmd/coder/sync.go b/cmd/coder/sync.go index 601fddd7..5b00a670 100644 --- a/cmd/coder/sync.go +++ b/cmd/coder/sync.go @@ -8,30 +8,37 @@ import ( "path/filepath" "strings" - "github.com/spf13/pflag" + "cdr.dev/coder-cli/internal/sync" + "github.com/urfave/cli" + "golang.org/x/xerrors" - "go.coder.com/cli" "go.coder.com/flog" - - "cdr.dev/coder-cli/internal/sync" ) -type syncCmd struct { - init bool -} - -func (cmd *syncCmd) Spec() cli.CommandSpec { - return cli.CommandSpec{ - Name: "sync", - Usage: "[local directory] [:]", - Desc: "establish a one way directory sync to a remote environment", +func makeSyncCmd() cli.Command { + var init bool + return cli.Command{ + Name: "sync", + Usage: "synchronize local files to a Coder environment", + Description: "Establish a one way directory sync to a Coder environment.", + ArgsUsage: "[local directory] [:]", + Before: func(c *cli.Context) error { + if c.Args().Get(0) == "" || c.Args().Get(1) == "" { + return xerrors.Errorf("[local] and [remote] arguments are required") + } + return nil + }, + Action: makeRunSync(&init), + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "init", + Usage: "do initial transfer and exit", + Destination: &init, + }, + }, } } -func (cmd *syncCmd) RegisterFlags(fl *pflag.FlagSet) { - fl.BoolVarP(&cmd.init, "init", "i", false, "do initial transfer and exit") -} - // version returns local rsync protocol version as a string. func rsyncVersion() string { cmd := exec.Command("rsync", "--version") @@ -49,63 +56,61 @@ func rsyncVersion() string { return versionString[1] } -func (cmd *syncCmd) Run(fl *pflag.FlagSet) { - var ( - local = fl.Arg(0) - remote = fl.Arg(1) - ) - if local == "" || remote == "" { - exitUsage(fl) - } - - entClient := requireAuth() - - info, err := os.Stat(local) - if err != nil { - flog.Fatal("%v", err) - } - if !info.IsDir() { - flog.Fatal("%s must be a directory", local) - } - - remoteTokens := strings.SplitN(remote, ":", 2) - if len(remoteTokens) != 2 { - flog.Fatal("remote misformatted") - } - var ( - envName = remoteTokens[0] - remoteDir = remoteTokens[1] - ) - - env := findEnv(entClient, envName) - - absLocal, err := filepath.Abs(local) - if err != nil { - flog.Fatal("make abs path out of %v: %v", local, absLocal) - } - - s := sync.Sync{ - Init: cmd.init, - Env: env, - RemoteDir: remoteDir, - LocalDir: absLocal, - Client: entClient, - } - - localVersion := rsyncVersion() - remoteVersion, rsyncErr := s.Version() - - if rsyncErr != nil { - flog.Info("Unable to determine remote rsync version. Proceeding cautiously.") - } else if localVersion != remoteVersion { - flog.Fatal("rsync protocol mismatch: local = %v, remote = %v", localVersion, rsyncErr) - } - - for err == nil || err == sync.ErrRestartSync { - err = s.Run() - } - - if err != nil { - flog.Fatal("%v", err) +func makeRunSync(init *bool) func(c *cli.Context) { + return func(c *cli.Context) { + var ( + local = c.Args().Get(0) + remote = c.Args().Get(1) + ) + + entClient := requireAuth() + + info, err := os.Stat(local) + if err != nil { + flog.Fatal("%v", err) + } + if !info.IsDir() { + flog.Fatal("%s must be a directory", local) + } + + remoteTokens := strings.SplitN(remote, ":", 2) + if len(remoteTokens) != 2 { + flog.Fatal("remote misformatted") + } + var ( + envName = remoteTokens[0] + remoteDir = remoteTokens[1] + ) + + env := findEnv(entClient, envName) + + absLocal, err := filepath.Abs(local) + if err != nil { + flog.Fatal("make abs path out of %v: %v", local, absLocal) + } + + s := sync.Sync{ + Init: *init, + Env: env, + RemoteDir: remoteDir, + LocalDir: absLocal, + Client: entClient, + } + + localVersion := rsyncVersion() + remoteVersion, rsyncErr := s.Version() + + if rsyncErr != nil { + flog.Info("Unable to determine remote rsync version. Proceeding cautiously.") + } else if localVersion != remoteVersion { + flog.Fatal("rsync protocol mismatch: local = %v, remote = %v", localVersion, rsyncErr) + } + + for err == nil || err == sync.ErrRestartSync { + err = s.Run() + } + if err != nil { + flog.Fatal("%v", err) + } } } diff --git a/internal/x/xvalidate/errors.go b/internal/x/xvalidate/errors.go deleted file mode 100644 index d502850c..00000000 --- a/internal/x/xvalidate/errors.go +++ /dev/null @@ -1,101 +0,0 @@ -package xvalidate - -import ( - "bytes" - "fmt" - - "github.com/spf13/pflag" - - "go.coder.com/flog" -) - -// cerrors contains a list of errors. -type cerrors struct { - cerrors []error -} - -func (e cerrors) writeTo(buf *bytes.Buffer) { - for i, err := range e.cerrors { - if err == nil { - continue - } - buf.WriteString(err.Error()) - // don't newline after last error - if i != len(e.cerrors)-1 { - buf.WriteRune('\n') - } - } -} - -func (e cerrors) Error() string { - buf := &bytes.Buffer{} - e.writeTo(buf) - return buf.String() -} - -// stripNils removes nil errors from the slice. -func stripNils(errs []error) []error { - // We can't range since errs may be resized - // during the loop. - for i := 0; i < len(errs); i++ { - err := errs[i] - if err == nil { - // shift down - copy(errs[i:], errs[i+1:]) - // pop off last element - errs = errs[:len(errs)-1] - } - } - return errs -} - -// flatten expands all parts of cerrors onto errs. -func flatten(errs []error) []error { - nerrs := make([]error, 0, len(errs)) - for _, err := range errs { - errs, ok := err.(cerrors) - if !ok { - nerrs = append(nerrs, err) - continue - } - nerrs = append(nerrs, errs.cerrors...) - } - return nerrs -} - -// combineErrors combines multiple errors into one -func combineErrors(errs ...error) error { - errs = stripNils(errs) - switch len(errs) { - case 0: - return nil - case 1: - return errs[0] - default: - // Don't return if all of the errors of nil. - for _, err := range errs { - if err != nil { - return cerrors{cerrors: flatten(errs)} - } - } - return nil - } -} - -// Validator is a command capable of validating its flags -type Validator interface { - Validate(fl *pflag.FlagSet) []error -} - -// Validate performs validation and exits with a nonzero status code if validation fails. -// The proper errors are printed to stderr. -func Validate(fl *pflag.FlagSet, v Validator) { - errs := v.Validate(fl) - - err := combineErrors(errs...) - if err != nil { - fl.Usage() - fmt.Println("") - flog.Fatal("failed to validate this command\n%v", err) - } -} From ab278994b81b77073ecd807e65326baa8d9423bf Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 31 Jul 2020 14:33:02 -0500 Subject: [PATCH 05/14] Rework usage and descriptions --- cmd/coder/configssh.go | 4 ++-- cmd/coder/envs.go | 7 +++---- cmd/coder/login.go | 11 +++++------ cmd/coder/logout.go | 7 +++---- cmd/coder/secrets.go | 27 +++++++++++++-------------- cmd/coder/shell.go | 7 +++---- cmd/coder/sync.go | 2 +- cmd/coder/users.go | 19 +++++-------------- 8 files changed, 35 insertions(+), 49 deletions(-) diff --git a/cmd/coder/configssh.go b/cmd/coder/configssh.go index f6d3b794..23f09d1d 100644 --- a/cmd/coder/configssh.go +++ b/cmd/coder/configssh.go @@ -26,8 +26,8 @@ func makeConfigSSHCmd() cli.Command { return cli.Command{ Name: "config-ssh", - UsageText: "", - Description: "add your Coder Enterprise environments to ~/.ssh/config", + 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{ diff --git a/cmd/coder/envs.go b/cmd/coder/envs.go index a83afb05..e7d6a2cd 100644 --- a/cmd/coder/envs.go +++ b/cmd/coder/envs.go @@ -9,14 +9,13 @@ import ( func makeEnvsCommand() cli.Command { return cli.Command{ Name: "envs", - UsageText: "", - Description: "interact with Coder environments", + 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", - UsageText: "", - Description: "", + Description: "List all Coder environments owned by the active user.", ArgsUsage: "[...flags]>", Action: func(c *cli.Context) { entClient := requireAuth() diff --git a/cmd/coder/login.go b/cmd/coder/login.go index 70da187e..7064d03c 100644 --- a/cmd/coder/login.go +++ b/cmd/coder/login.go @@ -16,13 +16,12 @@ import ( ) func makeLoginCmd() cli.Command { - cmd := cli.Command{ - Name: "login", - Usage: "[Coder Enterprise URL eg. http://my.coder.domain/ ]", - Description: "authenticate this client for future operations", - Action: login, + return cli.Command{ + Name: "login", + Usage: "Authenticate this client for future operations", + ArgsUsage: "[Coder Enterprise URL eg. http://my.coder.domain/]", + Action: login, } - return cmd } func login(c *cli.Context) { diff --git a/cmd/coder/logout.go b/cmd/coder/logout.go index e90d98f8..6bae109d 100644 --- a/cmd/coder/logout.go +++ b/cmd/coder/logout.go @@ -11,10 +11,9 @@ import ( func makeLogoutCmd() cli.Command { return cli.Command{ - Name: "logout", - //Usage: "", - Description: "remove local authentication credentials (if any)", - Action: logout, + Name: "logout", + Usage: "Remove local authentication credentials if any exist", + Action: logout, } } diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go index 91fa8c4e..42ccb1a9 100644 --- a/cmd/coder/secrets.go +++ b/cmd/coder/secrets.go @@ -17,27 +17,26 @@ import ( func makeSecretsCmd() cli.Command { return cli.Command{ Name: "secrets", - Usage: "", + Usage: "Interact with Coder Secrets", Description: "Interact with secrets objects owned by the active user.", Subcommands: []cli.Command{ { - Name: "ls", - Usage: "", - Description: "list all secrets owned by the active user", - Action: listSecrets, + Name: "ls", + Usage: "List all secrets owned by the active user", + Action: listSecrets, }, makeCreateSecret(), { - Name: "rm", - Usage: "", - Description: "", - Action: removeSecret, + Name: "rm", + Usage: "Remove a secret by name", + ArgsUsage: "[secret_name]", + Action: removeSecret, }, { - Name: "view", - Usage: "", - Description: "", - Action: viewSecret, + Name: "view", + Usage: "View a secret by name", + ArgsUsage: "[secret_name]", + Action: viewSecret, }, }, Flags: nil, @@ -54,7 +53,7 @@ func makeCreateSecret() cli.Command { return cli.Command{ Name: "create", - Usage: "create a new secret", + Usage: "Create a new secret", Description: "Create a new secret object to store application secrets and access them securely from within your environments.", ArgsUsage: "[secret_name]", Before: func(c *cli.Context) error { diff --git a/cmd/coder/shell.go b/cmd/coder/shell.go index c40933ea..c52ea656 100644 --- a/cmd/coder/shell.go +++ b/cmd/coder/shell.go @@ -22,12 +22,11 @@ import ( func makeShellCmd() cli.Command { return cli.Command{ Name: "sh", - Usage: " []", - Description: "execute a remote command on the environment\\nIf no command is specified, the default shell is opened.", + Usage: "Open a shell and execute commands in a Coder environment", + Description: "Execute a remote command on the environment\\nIf no command is specified, the default shell is opened.", + ArgsUsage: "[env_name] []", SkipFlagParsing: true, SkipArgReorder: true, - UsageText: "", - ArgsUsage: "", Action: shell, } } diff --git a/cmd/coder/sync.go b/cmd/coder/sync.go index 5b00a670..a213fdd5 100644 --- a/cmd/coder/sync.go +++ b/cmd/coder/sync.go @@ -19,7 +19,7 @@ func makeSyncCmd() cli.Command { var init bool return cli.Command{ Name: "sync", - Usage: "synchronize local files to a Coder environment", + Usage: "Synchronize local files to a Coder environment", Description: "Establish a one way directory sync to a Coder environment.", ArgsUsage: "[local directory] [:]", Before: func(c *cli.Context) error { diff --git a/cmd/coder/users.go b/cmd/coder/users.go index 0d1fc031..bc800162 100644 --- a/cmd/coder/users.go +++ b/cmd/coder/users.go @@ -14,28 +14,19 @@ import ( func makeUsersCmd() cli.Command { var output string return cli.Command{ - Name: "users", - ShortName: "", - Aliases: nil, - Usage: "[subcommand] ", - UsageText: "", - Description: "", - ArgsUsage: "", - Action: nil, - OnUsageError: nil, + Name: "users", + Usage: "Interact with Coder user accounts", + ArgsUsage: "[subcommand] ", Subcommands: []cli.Command{ { Name: "ls", - Usage: "", - UsageText: "", + Usage: "list all user accounts", Description: "", - ArgsUsage: "", - Category: "", Action: listUsers(&output), Flags: []cli.Flag{ cli.StringFlag{ Name: "output", - Usage: "", + Usage: "(json | human)", Required: false, Value: "human", Destination: &output, From b6938372d5a948486f56fca5c0e4d66484bf391e Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 31 Jul 2020 14:35:52 -0500 Subject: [PATCH 06/14] fixup! Rework usage and descriptions --- cmd/coder/configssh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/coder/configssh.go b/cmd/coder/configssh.go index 23f09d1d..20a9257a 100644 --- a/cmd/coder/configssh.go +++ b/cmd/coder/configssh.go @@ -122,7 +122,7 @@ func configSSH(filepath *string, remove *bool) func(c *cli.Context) { 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("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) From e1d956895eaa91852a71c14b2588a28dfb590fad Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 31 Jul 2020 14:50:41 -0500 Subject: [PATCH 07/14] fixup! fixup! Rework usage and descriptions --- ci/integration/integration_test.go | 17 ++++++++++------- ci/integration/secrets_test.go | 1 - 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index 098a4791..f933aac1 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -40,11 +40,10 @@ func TestCoderCLI(t *testing.T) { 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) @@ -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(), ) @@ -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"), ) @@ -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(), ) } diff --git a/ci/integration/secrets_test.go b/ci/integration/secrets_test.go index b77c345b..58ba2404 100644 --- a/ci/integration/secrets_test.go +++ b/ci/integration/secrets_test.go @@ -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 From 748409ebafddd775eb9cbf984da3887f11c98f98 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Fri, 31 Jul 2020 17:38:22 -0500 Subject: [PATCH 08/14] More improvements --- cmd/coder/main.go | 6 ++++++ cmd/coder/secrets.go | 27 ++++++++++++++++++--------- cmd/coder/users.go | 13 +++++-------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 8b049d85..b9b273f3 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -35,6 +35,12 @@ func main() { app.Name = "coder" app.Usage = "coder provides a CLI for working with an existing Coder Enterprise installation" app.Version = fmt.Sprintf("%s %s %s/%s", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) + app.Author = "Coder Technologies Inc." + app.CommandNotFound = func(c *cli.Context, s string) { + flog.Fatal("command %q not found", s) + } + app.Email = "support@coder.com" + app.Commands = []cli.Command{ makeLoginCmd(), makeLogoutCmd(), diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go index 42ccb1a9..1d07feb5 100644 --- a/cmd/coder/secrets.go +++ b/cmd/coder/secrets.go @@ -28,8 +28,8 @@ func makeSecretsCmd() cli.Command { makeCreateSecret(), { Name: "rm", - Usage: "Remove a secret by name", - ArgsUsage: "[secret_name]", + Usage: "Remove one or more secrets by name", + ArgsUsage: "[...secret_name]", Action: removeSecret, }, { @@ -175,14 +175,23 @@ func viewSecret(c *cli.Context) { func removeSecret(c *cli.Context) { var ( client = requireAuth() - name = c.Args().First() + names = append([]string{c.Args().First()}, c.Args().Tail()...) ) - if name == "" { - flog.Fatal("[name] is a required argument") + if len(names) < 1 || names[0] == "" { + flog.Fatal("[...secret_name] is a required argument") } - err := client.DeleteSecretByName(name) - requireSuccess(err, "failed to delete secret: %v", err) - - flog.Info("Successfully deleted secret %q", name) + errorSeen := false + for _, n := range names { + err := client.DeleteSecretByName(n) + if err != nil { + flog.Error("failed to delete secret: %v", err) + errorSeen = true + } else { + flog.Info("Successfully deleted secret %q", n) + } + } + if errorSeen { + os.Exit(1) + } } diff --git a/cmd/coder/users.go b/cmd/coder/users.go index bc800162..77bab1dd 100644 --- a/cmd/coder/users.go +++ b/cmd/coder/users.go @@ -14,20 +14,17 @@ import ( func makeUsersCmd() cli.Command { var output string return cli.Command{ - Name: "users", - Usage: "Interact with Coder user accounts", - ArgsUsage: "[subcommand] ", + Name: "users", + Usage: "Interact with Coder user accounts", Subcommands: []cli.Command{ { - Name: "ls", - Usage: "list all user accounts", - Description: "", - Action: listUsers(&output), + Name: "ls", + Usage: "list all user accounts", + Action: listUsers(&output), Flags: []cli.Flag{ cli.StringFlag{ Name: "output", Usage: "(json | human)", - Required: false, Value: "human", Destination: &output, }, From a27700e2876965d595bd2943d03aa6b1f754b6b8 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Sat, 1 Aug 2020 12:20:21 -0500 Subject: [PATCH 09/14] Add `tab:"omit"` struct tag --- cmd/coder/secrets.go | 1 - cmd/coder/sync.go | 7 +++---- cmd/coder/users.go | 1 - internal/entclient/secrets.go | 4 ++-- internal/x/xtabwriter/tabwriter.go | 25 +++++++++++++++++++++---- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go index 1d07feb5..26970f25 100644 --- a/cmd/coder/secrets.go +++ b/cmd/coder/secrets.go @@ -39,7 +39,6 @@ func makeSecretsCmd() cli.Command { Action: viewSecret, }, }, - Flags: nil, } } diff --git a/cmd/coder/sync.go b/cmd/coder/sync.go index a213fdd5..7f2c1f39 100644 --- a/cmd/coder/sync.go +++ b/cmd/coder/sync.go @@ -18,10 +18,9 @@ import ( func makeSyncCmd() cli.Command { var init bool return cli.Command{ - Name: "sync", - Usage: "Synchronize local files to a Coder environment", - Description: "Establish a one way directory sync to a Coder environment.", - ArgsUsage: "[local directory] [:]", + Name: "sync", + Usage: "Establish a one way directory sync to a Coder environment", + ArgsUsage: "[local directory] [:]", Before: func(c *cli.Context) error { if c.Args().Get(0) == "" || c.Args().Get(1) == "" { return xerrors.Errorf("[local] and [remote] arguments are required") diff --git a/cmd/coder/users.go b/cmd/coder/users.go index 77bab1dd..54251c50 100644 --- a/cmd/coder/users.go +++ b/cmd/coder/users.go @@ -31,7 +31,6 @@ func makeUsersCmd() cli.Command { }, }, }, - HelpName: "", } } diff --git a/internal/entclient/secrets.go b/internal/entclient/secrets.go index 98fa543b..4273c4bc 100644 --- a/internal/entclient/secrets.go +++ b/internal/entclient/secrets.go @@ -7,12 +7,12 @@ import ( // Secret describes a Coder secret type Secret struct { - ID string `json:"id"` + ID string `json:"id" tab:"omit"` Name string `json:"name"` Value string `json:"value,omitempty"` Description string `json:"description"` CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + UpdatedAt time.Time `json:"updated_at" tab:"omit"` } // Secrets gets all secrets owned by the authed user diff --git a/internal/x/xtabwriter/tabwriter.go b/internal/x/xtabwriter/tabwriter.go index 1c8b1167..44a768fd 100644 --- a/internal/x/xtabwriter/tabwriter.go +++ b/internal/x/xtabwriter/tabwriter.go @@ -8,27 +8,44 @@ import ( "text/tabwriter" ) -// NewWriter chooses reasonable defaults for a human readable output of tabular data +const structFieldTagKey = "tab" + +// NewWriter chooses reasonable defaults for a human readable output of tabular data. func NewWriter() *tabwriter.Writer { return tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) } -// StructValues tab delimits the values of a given struct +// StructValues tab delimits the values of a given struct. +// +// Tag a field `tab:"omit"` to hide it from output. func StructValues(data interface{}) string { v := reflect.ValueOf(data) s := &strings.Builder{} for i := 0; i < v.NumField(); i++ { + if shouldHideField(v.Type().Field(i)) { + continue + } s.WriteString(fmt.Sprintf("%s\t", v.Field(i).Interface())) } return s.String() } -// StructFieldNames tab delimits the field names of a given struct +// StructFieldNames tab delimits the field names of a given struct. +// +// Tag a field `tab:"omit"` to hide it from output. func StructFieldNames(data interface{}) string { v := reflect.ValueOf(data) s := &strings.Builder{} for i := 0; i < v.NumField(); i++ { - s.WriteString(fmt.Sprintf("%s\t", v.Type().Field(i).Name)) + field := v.Type().Field(i) + if shouldHideField(field) { + continue + } + s.WriteString(fmt.Sprintf("%s\t", field.Name)) } return s.String() } + +func shouldHideField(f reflect.StructField) bool { + return f.Tag.Get(structFieldTagKey) == "omit" +} From 66528ca9181c7e685ed5342a53ebd6879ae96661 Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Sat, 1 Aug 2020 19:02:15 -0500 Subject: [PATCH 10/14] fixup! Add `tab:"omit"` struct tag --- internal/entclient/me.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/entclient/me.go b/internal/entclient/me.go index 2b44debb..526f9259 100644 --- a/internal/entclient/me.go +++ b/internal/entclient/me.go @@ -6,11 +6,12 @@ import ( // User describes a Coder user account type User struct { - ID string `json:"id"` + ID string `json:"id" tab:"omit"` Email string `json:"email"` Username string `json:"username"` Name string `json:"name"` CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at" tab:"omit"` } // Me gets the details of the authenticated user From 5c755707aa1ea46f36cf1da2ccab6c38acca3dca Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Sat, 1 Aug 2020 19:12:34 -0500 Subject: [PATCH 11/14] fixup! fixup! Add `tab:"omit"` struct tag --- internal/entclient/me.go | 4 ++-- internal/entclient/secrets.go | 4 ++-- internal/x/xtabwriter/tabwriter.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/entclient/me.go b/internal/entclient/me.go index 526f9259..2d57d8cc 100644 --- a/internal/entclient/me.go +++ b/internal/entclient/me.go @@ -6,12 +6,12 @@ import ( // User describes a Coder user account type User struct { - ID string `json:"id" tab:"omit"` + ID string `json:"id" tab:"-"` Email string `json:"email"` Username string `json:"username"` Name string `json:"name"` CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at" tab:"omit"` + UpdatedAt time.Time `json:"updated_at" tab:"-"` } // Me gets the details of the authenticated user diff --git a/internal/entclient/secrets.go b/internal/entclient/secrets.go index 4273c4bc..72a62105 100644 --- a/internal/entclient/secrets.go +++ b/internal/entclient/secrets.go @@ -7,12 +7,12 @@ import ( // Secret describes a Coder secret type Secret struct { - ID string `json:"id" tab:"omit"` + ID string `json:"id" tab:"-"` Name string `json:"name"` Value string `json:"value,omitempty"` Description string `json:"description"` CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at" tab:"omit"` + UpdatedAt time.Time `json:"updated_at" tab:"-"` } // Secrets gets all secrets owned by the authed user diff --git a/internal/x/xtabwriter/tabwriter.go b/internal/x/xtabwriter/tabwriter.go index 44a768fd..4f3b3687 100644 --- a/internal/x/xtabwriter/tabwriter.go +++ b/internal/x/xtabwriter/tabwriter.go @@ -17,7 +17,7 @@ func NewWriter() *tabwriter.Writer { // StructValues tab delimits the values of a given struct. // -// Tag a field `tab:"omit"` to hide it from output. +// Tag a field `tab:"-"` to hide it from output. func StructValues(data interface{}) string { v := reflect.ValueOf(data) s := &strings.Builder{} @@ -32,7 +32,7 @@ func StructValues(data interface{}) string { // StructFieldNames tab delimits the field names of a given struct. // -// Tag a field `tab:"omit"` to hide it from output. +// Tag a field `tab:"-"` to hide it from output. func StructFieldNames(data interface{}) string { v := reflect.ValueOf(data) s := &strings.Builder{} @@ -47,5 +47,5 @@ func StructFieldNames(data interface{}) string { } func shouldHideField(f reflect.StructField) bool { - return f.Tag.Get(structFieldTagKey) == "omit" + return f.Tag.Get(structFieldTagKey) == "-" } From 2cc7d2d7411f7b55d8adf7058fa365414b714f9e Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Sat, 1 Aug 2020 22:09:29 -0500 Subject: [PATCH 12/14] Add table output to envs ls --- ci/integration/integration_test.go | 2 +- cmd/coder/envs.go | 12 ++++++++++- cmd/coder/main.go | 5 +++++ cmd/coder/secrets.go | 1 + cmd/coder/users.go | 5 +++-- internal/entclient/env.go | 24 ++++++++++++++++++++-- internal/x/xjson/duration.go | 33 ++++++++++++++++++++++++++++++ internal/x/xtabwriter/tabwriter.go | 2 +- 8 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 internal/x/xjson/duration.go diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index f933aac1..2ee1b139 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -49,7 +49,7 @@ func TestCoderCLI(t *testing.T) { headlessLogin(ctx, t, c) c.Run(ctx, "coder envs").Assert(t, - tcli.Success(), + tcli.Error(), ) c.Run(ctx, "coder envs ls").Assert(t, diff --git a/cmd/coder/envs.go b/cmd/coder/envs.go index e7d6a2cd..dcc23cff 100644 --- a/cmd/coder/envs.go +++ b/cmd/coder/envs.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "cdr.dev/coder-cli/internal/x/xtabwriter" "github.com/urfave/cli" ) @@ -11,6 +12,7 @@ func makeEnvsCommand() cli.Command { Name: "envs", Usage: "Interact with Coder environments", Description: "Perform operations on the Coder environments owned by the active user.", + Action: exitHelp, Subcommands: []cli.Command{ { Name: "ls", @@ -21,9 +23,17 @@ func makeEnvsCommand() cli.Command { entClient := requireAuth() envs := getEnvs(entClient) + w := xtabwriter.NewWriter() + if len(envs) > 0 { + _, err := fmt.Fprintln(w, xtabwriter.StructFieldNames(envs[0])) + requireSuccess(err, "failed to write header: %v", err) + } for _, env := range envs { - fmt.Println(env.Name) + _, err := fmt.Fprintln(w, xtabwriter.StructValues(env)) + requireSuccess(err, "failed to write row: %v", err) } + err := w.Flush() + requireSuccess(err, "failed to flush tab writer: %v", err) }, Flags: nil, }, diff --git a/cmd/coder/main.go b/cmd/coder/main.go index b9b273f3..bbbdcd9f 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -40,6 +40,7 @@ func main() { flog.Fatal("command %q not found", s) } app.Email = "support@coder.com" + app.Action = exitHelp app.Commands = []cli.Command{ makeLoginCmd(), @@ -63,3 +64,7 @@ func requireSuccess(err error, msg string, args ...interface{}) { flog.Fatal(msg, args...) } } + +func exitHelp(c *cli.Context) { + cli.ShowCommandHelpAndExit(c, c.Command.FullName(), 1) +} diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go index 26970f25..eead2166 100644 --- a/cmd/coder/secrets.go +++ b/cmd/coder/secrets.go @@ -19,6 +19,7 @@ func makeSecretsCmd() cli.Command { Name: "secrets", Usage: "Interact with Coder Secrets", Description: "Interact with secrets objects owned by the active user.", + Action: exitHelp, Subcommands: []cli.Command{ { Name: "ls", diff --git a/cmd/coder/users.go b/cmd/coder/users.go index 54251c50..6f2fb3ed 100644 --- a/cmd/coder/users.go +++ b/cmd/coder/users.go @@ -14,8 +14,9 @@ import ( func makeUsersCmd() cli.Command { var output string return cli.Command{ - Name: "users", - Usage: "Interact with Coder user accounts", + Name: "users", + Usage: "Interact with Coder user accounts", + Action: exitHelp, Subcommands: []cli.Command{ { Name: "ls", diff --git a/internal/entclient/env.go b/internal/entclient/env.go index 11a806c5..508f438f 100644 --- a/internal/entclient/env.go +++ b/internal/entclient/env.go @@ -4,13 +4,33 @@ import ( "context" "time" + "cdr.dev/coder-cli/internal/x/xjson" "nhooyr.io/websocket" ) // Environment describes a Coder environment type Environment struct { - Name string `json:"name"` - ID string `json:"id"` + ID string `json:"id" tab:"-"` + Name string `json:"name"` + ImageID string `json:"image_id" tab:"-"` + ImageTag string `json:"image_tag"` + OrganizationID string `json:"organization_id" tab:"-"` + UserID string `json:"user_id" tab:"-"` + LastBuiltAt time.Time `json:"last_built_at" tab:"-"` + CPUCores float32 `json:"cpu_cores"` + MemoryGB int `json:"memory_gb"` + DiskGB int `json:"disk_gb"` + GPUs int `json:"gpus"` + Updating bool `json:"updating"` + RebuildMessages []struct { + Text string `json:"text"` + Required bool `json:"required"` + } `json:"rebuild_messages" tab:"-"` + CreatedAt time.Time `json:"created_at" tab:"-"` + UpdatedAt time.Time `json:"updated_at" tab:"-"` + LastOpenedAt time.Time `json:"last_opened_at" tab:"-"` + LastConnectionAt time.Time `json:"last_connection_at" tab:"-"` + AutoOffThreshold xjson.Duration `json:"auto_off_threshold" tab:"-"` } // Envs gets the list of environments owned by the authenticated user diff --git a/internal/x/xjson/duration.go b/internal/x/xjson/duration.go new file mode 100644 index 00000000..3ec08b48 --- /dev/null +++ b/internal/x/xjson/duration.go @@ -0,0 +1,33 @@ +package xjson + +import ( + "encoding/json" + "strconv" + "time" +) + +// Duration is a time.Duration that marshals to millisecond precision. +// Most javascript applications expect durations to be in milliseconds. +type Duration time.Duration + +// MarshalJSON marshals the duration to millisecond precision. +func (d Duration) MarshalJSON() ([]byte, error) { + du := time.Duration(d) + return json.Marshal(du.Milliseconds()) +} + +// UnmarshalJSON unmarshals a millisecond-precision integer to +// a time.Duration. +func (d *Duration) UnmarshalJSON(b []byte) error { + i, err := strconv.ParseInt(string(b), 10, 64) + if err != nil { + return err + } + + *d = Duration(time.Duration(i) * time.Millisecond) + return nil +} + +func (d Duration) String() string { + return time.Duration(d).String() +} diff --git a/internal/x/xtabwriter/tabwriter.go b/internal/x/xtabwriter/tabwriter.go index 4f3b3687..017169f0 100644 --- a/internal/x/xtabwriter/tabwriter.go +++ b/internal/x/xtabwriter/tabwriter.go @@ -25,7 +25,7 @@ func StructValues(data interface{}) string { if shouldHideField(v.Type().Field(i)) { continue } - s.WriteString(fmt.Sprintf("%s\t", v.Field(i).Interface())) + s.WriteString(fmt.Sprintf("%v\t", v.Field(i).Interface())) } return s.String() } From bfa54f11e598a7878faa6291624d93782299aeab Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Sun, 2 Aug 2020 18:53:10 -0500 Subject: [PATCH 13/14] Add json output to envs ls --- cmd/coder/envs.go | 42 +++++++++++++++++++++++++++++++----------- cmd/coder/secrets.go | 4 ++-- cmd/coder/users.go | 2 +- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/cmd/coder/envs.go b/cmd/coder/envs.go index dcc23cff..cac02d85 100644 --- a/cmd/coder/envs.go +++ b/cmd/coder/envs.go @@ -1,13 +1,18 @@ package main import ( + "encoding/json" "fmt" + "os" "cdr.dev/coder-cli/internal/x/xtabwriter" "github.com/urfave/cli" + + "go.coder.com/flog" ) func makeEnvsCommand() cli.Command { + var outputFmt string return cli.Command{ Name: "envs", Usage: "Interact with Coder environments", @@ -23,19 +28,34 @@ func makeEnvsCommand() cli.Command { entClient := requireAuth() envs := getEnvs(entClient) - w := xtabwriter.NewWriter() - if len(envs) > 0 { - _, err := fmt.Fprintln(w, xtabwriter.StructFieldNames(envs[0])) - requireSuccess(err, "failed to write header: %v", err) - } - for _, env := range envs { - _, err := fmt.Fprintln(w, xtabwriter.StructValues(env)) - requireSuccess(err, "failed to write row: %v", err) + switch outputFmt { + case "human": + w := xtabwriter.NewWriter() + if len(envs) > 0 { + _, err := fmt.Fprintln(w, xtabwriter.StructFieldNames(envs[0])) + requireSuccess(err, "failed to write header: %v", err) + } + for _, env := range envs { + _, err := fmt.Fprintln(w, xtabwriter.StructValues(env)) + requireSuccess(err, "failed to write row: %v", err) + } + err := w.Flush() + requireSuccess(err, "failed to flush tab writer: %v", err) + case "json": + err := json.NewEncoder(os.Stdout).Encode(envs) + requireSuccess(err, "failed to write json: %v", err) + default: + flog.Fatal("unknown --output value %q", outputFmt) } - err := w.Flush() - requireSuccess(err, "failed to flush tab writer: %v", err) }, - Flags: nil, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "output", + Usage: "json | human", + Value: "human", + Destination: &outputFmt, + }, + }, }, }, } diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go index eead2166..fd6d222b 100644 --- a/cmd/coder/secrets.go +++ b/cmd/coder/secrets.go @@ -31,7 +31,7 @@ func makeSecretsCmd() cli.Command { Name: "rm", Usage: "Remove one or more secrets by name", ArgsUsage: "[...secret_name]", - Action: removeSecret, + Action: removeSecrets, }, { Name: "view", @@ -172,7 +172,7 @@ func viewSecret(c *cli.Context) { requireSuccess(err, "failed to write: %v", err) } -func removeSecret(c *cli.Context) { +func removeSecrets(c *cli.Context) { var ( client = requireAuth() names = append([]string{c.Args().First()}, c.Args().Tail()...) diff --git a/cmd/coder/users.go b/cmd/coder/users.go index 6f2fb3ed..c279a3e4 100644 --- a/cmd/coder/users.go +++ b/cmd/coder/users.go @@ -25,7 +25,7 @@ func makeUsersCmd() cli.Command { Flags: []cli.Flag{ cli.StringFlag{ Name: "output", - Usage: "(json | human)", + Usage: "json | human", Value: "human", Destination: &output, }, From b0e06c41981eda5f87736f198e6c103d26b44f6c Mon Sep 17 00:00:00 2001 From: Charlie Moog Date: Mon, 3 Aug 2020 08:50:27 -0500 Subject: [PATCH 14/14] Abstract table writing to xtabwriter --- cmd/coder/envs.go | 16 ++++------------ cmd/coder/users.go | 16 ++++------------ internal/x/xtabwriter/tabwriter.go | 26 ++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/cmd/coder/envs.go b/cmd/coder/envs.go index cac02d85..c3eeba5f 100644 --- a/cmd/coder/envs.go +++ b/cmd/coder/envs.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "os" "cdr.dev/coder-cli/internal/x/xtabwriter" @@ -30,17 +29,10 @@ func makeEnvsCommand() cli.Command { switch outputFmt { case "human": - w := xtabwriter.NewWriter() - if len(envs) > 0 { - _, err := fmt.Fprintln(w, xtabwriter.StructFieldNames(envs[0])) - requireSuccess(err, "failed to write header: %v", err) - } - for _, env := range envs { - _, err := fmt.Fprintln(w, xtabwriter.StructValues(env)) - requireSuccess(err, "failed to write row: %v", err) - } - err := w.Flush() - requireSuccess(err, "failed to flush tab writer: %v", err) + err := xtabwriter.WriteTable(len(envs), func(i int) interface{} { + return envs[i] + }) + requireSuccess(err, "failed to write table: %v", err) case "json": err := json.NewEncoder(os.Stdout).Encode(envs) requireSuccess(err, "failed to write json: %v", err) diff --git a/cmd/coder/users.go b/cmd/coder/users.go index c279a3e4..dcf8c0d4 100644 --- a/cmd/coder/users.go +++ b/cmd/coder/users.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "os" "cdr.dev/coder-cli/internal/x/xtabwriter" @@ -44,17 +43,10 @@ func listUsers(outputFmt *string) func(c *cli.Context) { switch *outputFmt { case "human": - w := xtabwriter.NewWriter() - if len(users) > 0 { - _, err = fmt.Fprintln(w, xtabwriter.StructFieldNames(users[0])) - requireSuccess(err, "failed to write: %v", err) - } - for _, u := range users { - _, err = fmt.Fprintln(w, xtabwriter.StructValues(u)) - requireSuccess(err, "failed to write: %v", err) - } - err = w.Flush() - requireSuccess(err, "failed to flush writer: %v", err) + err := xtabwriter.WriteTable(len(users), func(i int) interface{} { + return users[i] + }) + requireSuccess(err, "failed to write table: %v", err) case "json": err = json.NewEncoder(os.Stdout).Encode(users) requireSuccess(err, "failed to encode users to json: %v", err) diff --git a/internal/x/xtabwriter/tabwriter.go b/internal/x/xtabwriter/tabwriter.go index 017169f0..1345cca7 100644 --- a/internal/x/xtabwriter/tabwriter.go +++ b/internal/x/xtabwriter/tabwriter.go @@ -46,6 +46,32 @@ func StructFieldNames(data interface{}) string { return s.String() } +// WriteTable writes the given list elements to stdout in a human readable +// tabular format. Headers abide by the `tab` struct tag. +// +// `tab:"-"` omits the field and no tag defaults to the Go identifier. +func WriteTable(length int, each func(i int) interface{}) error { + if length < 1 { + return nil + } + w := NewWriter() + defer w.Flush() + for ix := 0; ix < length; ix++ { + item := each(ix) + if ix == 0 { + _, err := fmt.Fprintln(w, StructFieldNames(item)) + if err != nil { + return err + } + } + _, err := fmt.Fprintln(w, StructValues(item)) + if err != nil { + return err + } + } + return nil +} + func shouldHideField(f reflect.StructField) bool { return f.Tag.Get(structFieldTagKey) == "-" }