From bc391d9ad9282f8e327fbb85eed7d569912398e7 Mon Sep 17 00:00:00 2001 From: Charles Moog Date: Wed, 10 Mar 2021 13:51:36 -0600 Subject: [PATCH 1/6] chore: limit direct uses of os.Stdout/os.Stderr/os.Stdin (#278) By moving the out/err/in channels to the cobra config, it will be easier to add fast and isolated unit tests. --- internal/cmd/cmd.go | 10 ++++----- internal/cmd/envs.go | 3 +-- internal/cmd/images.go | 3 +-- internal/cmd/login.go | 14 ++++++------ internal/cmd/rebuild.go | 4 +--- internal/cmd/resourcemanager.go | 3 +-- internal/cmd/shell.go | 38 +++++++++++++++++---------------- internal/cmd/sync.go | 14 +++++++----- internal/cmd/tags.go | 3 +-- internal/cmd/tokens.go | 3 +-- internal/cmd/urls.go | 3 +-- internal/cmd/users.go | 3 +-- internal/sync/sync.go | 22 +++++++++++-------- internal/sync/title.go | 7 ++---- pkg/clog/clog.go | 16 ++++++++++---- pkg/clog/clog_test.go | 6 +++--- 16 files changed, 78 insertions(+), 74 deletions(-) diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index ddc39c9c..8a7a7d21 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -2,8 +2,6 @@ package cmd import ( - "os" - "github.com/spf13/cobra" "github.com/spf13/cobra/doc" @@ -106,13 +104,13 @@ $ coder completion fish > ~/.config/fish/completions/coder.fish Run: func(cmd *cobra.Command, args []string) { switch args[0] { case "bash": - _ = cmd.Root().GenBashCompletion(os.Stdout) // Best effort. + _ = cmd.Root().GenBashCompletion(cmd.OutOrStdout()) // Best effort. case "zsh": - _ = cmd.Root().GenZshCompletion(os.Stdout) // Best effort. + _ = cmd.Root().GenZshCompletion(cmd.OutOrStdout()) // Best effort. case "fish": - _ = cmd.Root().GenFishCompletion(os.Stdout, true) // Best effort. + _ = cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true) // Best effort. case "powershell": - _ = cmd.Root().GenPowerShellCompletion(os.Stdout) // Best effort. + _ = cmd.Root().GenPowerShellCompletion(cmd.OutOrStdout()) // Best effort. } }, } diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index 8560748c..73d71dd8 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -8,7 +8,6 @@ import ( "io" "io/ioutil" "net/url" - "os" "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/coderutil" @@ -82,7 +81,7 @@ func lsEnvsCommand() *cobra.Command { return xerrors.Errorf("write table: %w", err) } case jsonOutput: - err := json.NewEncoder(os.Stdout).Encode(envs) + err := json.NewEncoder(cmd.OutOrStdout()).Encode(envs) if err != nil { return xerrors.Errorf("write environments as JSON: %w", err) } diff --git a/internal/cmd/images.go b/internal/cmd/images.go index b4ee6158..ff47bf61 100644 --- a/internal/cmd/images.go +++ b/internal/cmd/images.go @@ -2,7 +2,6 @@ package cmd import ( "encoding/json" - "os" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -62,7 +61,7 @@ func lsImgsCommand(user *string) *cobra.Command { switch outputFmt { case jsonOutput: - enc := json.NewEncoder(os.Stdout) + enc := json.NewEncoder(cmd.OutOrStdout()) // pretty print the json enc.SetIndent("", "\t") diff --git a/internal/cmd/login.go b/internal/cmd/login.go index cbb0f38f..691178bf 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -4,8 +4,8 @@ import ( "bufio" "context" "fmt" + "io" "net/url" - "os" "strings" "github.com/pkg/browser" @@ -40,7 +40,7 @@ func loginCmd() *cobra.Command { // From this point, the commandline is correct. // Don't return errors as it would print the usage. - if err := login(cmd.Context(), u); err != nil { + if err := login(cmd, u); err != nil { return xerrors.Errorf("login error: %w", err) } return nil @@ -60,7 +60,7 @@ func storeConfig(envURL *url.URL, sessionToken string, urlCfg, sessionCfg config return nil } -func login(ctx context.Context, envURL *url.URL) error { +func login(cmd *cobra.Command, envURL *url.URL) error { authURL := *envURL authURL.Path = envURL.Path + "/internal-auth" q := authURL.Query() @@ -73,8 +73,8 @@ func login(ctx context.Context, envURL *url.URL) error { fmt.Printf("Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String()) } - token := readLine("Paste token here: ") - if err := pingAPI(ctx, envURL, token); err != nil { + token := readLine("Paste token here: ", cmd.InOrStdin()) + if err := pingAPI(cmd.Context(), envURL, token); err != nil { return xerrors.Errorf("ping API with credentials: %w", err) } if err := storeConfig(envURL, token, config.URL, config.Session); err != nil { @@ -84,8 +84,8 @@ func login(ctx context.Context, envURL *url.URL) error { return nil } -func readLine(prompt string) string { - reader := bufio.NewReader(os.Stdin) +func readLine(prompt string, r io.Reader) string { + reader := bufio.NewReader(r) fmt.Print(prompt) text, _ := reader.ReadString('\n') return strings.TrimSuffix(text, "\n") diff --git a/internal/cmd/rebuild.go b/internal/cmd/rebuild.go index 978b3f58..f2fbbe8e 100644 --- a/internal/cmd/rebuild.go +++ b/internal/cmd/rebuild.go @@ -3,7 +3,6 @@ package cmd import ( "context" "fmt" - "os" "strings" "time" @@ -11,7 +10,6 @@ import ( "github.com/fatih/color" "github.com/manifoldco/promptui" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" "golang.org/x/xerrors" "cdr.dev/coder-cli/coder-sdk" @@ -85,7 +83,7 @@ func trailBuildLogs(ctx context.Context, client coder.Client, envID string) erro newSpinner := func() *spinner.Spinner { return spinner.New(spinner.CharSets[11], 100*time.Millisecond) } // this tells us whether to show dynamic loaders when printing output - isTerminal := terminal.IsTerminal(int(os.Stdout.Fd())) + isTerminal := showInteractiveOutput logs, err := client.FollowEnvironmentBuildLog(ctx, envID) if err != nil { diff --git a/internal/cmd/resourcemanager.go b/internal/cmd/resourcemanager.go index 775596e1..01f4875c 100644 --- a/internal/cmd/resourcemanager.go +++ b/internal/cmd/resourcemanager.go @@ -3,7 +3,6 @@ package cmd import ( "fmt" "io" - "os" "sort" "text/tabwriter" @@ -96,7 +95,7 @@ func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args [ return xerrors.Errorf("unknown --group %q", options.group) } - return printResourceTop(os.Stdout, groups, labeler, options.showEmptyGroups, options.sortBy) + return printResourceTop(cmd.OutOrStdout(), groups, labeler, options.showEmptyGroups, options.sortBy) } } diff --git a/internal/cmd/shell.go b/internal/cmd/shell.go index 35df969d..d33dbbbf 100644 --- a/internal/cmd/shell.go +++ b/internal/cmd/shell.go @@ -24,6 +24,12 @@ import ( "cdr.dev/coder-cli/pkg/clog" ) +var ( + showInteractiveOutput = terminal.IsTerminal(int(os.Stdout.Fd())) + outputFd = os.Stdout.Fd() + inputFd = os.Stdin.Fd() +) + func getEnvsForCompletion(user string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ctx := cmd.Context() @@ -146,15 +152,14 @@ func shell(cmd *cobra.Command, cmdArgs []string) error { } // TODO: Verify this is the correct behavior - isInteractive := terminal.IsTerminal(int(os.Stdout.Fd())) - if isInteractive { // checkAndRebuildEnvironment requires an interactive shell + if showInteractiveOutput { // checkAndRebuildEnvironment requires an interactive shell // Checks & Rebuilds the environment if needed. if err := checkAndRebuildEnvironment(ctx, client, env); err != nil { return err } } - if err := runCommand(ctx, client, env, command, args); err != nil { + if err := runCommand(cmd, client, env, command, args); err != nil { if exitErr, ok := err.(wsep.ExitError); ok { os.Exit(exitErr.Code) } @@ -309,26 +314,23 @@ func sendResizeEvents(ctx context.Context, termFD uintptr, process wsep.Process) } } -func runCommand(ctx context.Context, client coder.Client, env *coder.Environment, command string, args []string) error { - termFD := os.Stdout.Fd() - - isInteractive := terminal.IsTerminal(int(termFD)) - if isInteractive { +func runCommand(cmd *cobra.Command, client coder.Client, env *coder.Environment, command string, args []string) error { + if showInteractiveOutput { // If the client has a tty, take over it by setting the raw mode. // This allows for all input to be directly forwarded to the remote process, // otherwise, the local terminal would buffer input, interpret special keys, etc. - stdinState, err := xterminal.MakeRaw(os.Stdin.Fd()) + stdinState, err := xterminal.MakeRaw(inputFd) if err != nil { return err } defer func() { // Best effort. If this fails it will result in a broken terminal, // but there is nothing we can do about it. - _ = xterminal.Restore(os.Stdin.Fd(), stdinState) + _ = xterminal.Restore(inputFd, stdinState) }() } - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() conn, err := coderutil.DialEnvWsep(ctx, client, env) @@ -338,7 +340,7 @@ func runCommand(ctx context.Context, client coder.Client, env *coder.Environment go heartbeat(ctx, conn, 15*time.Second) var cmdEnv []string - if isInteractive { + if showInteractiveOutput { term := os.Getenv("TERM") if term == "" { term = "xterm" @@ -350,7 +352,7 @@ func runCommand(ctx context.Context, client coder.Client, env *coder.Environment process, err := execer.Start(ctx, wsep.Command{ Command: command, Args: args, - TTY: isInteractive, + TTY: showInteractiveOutput, Stdin: true, Env: cmdEnv, }) @@ -363,8 +365,8 @@ func runCommand(ctx context.Context, client coder.Client, env *coder.Environment } // Now that the remote process successfully started, if we have a tty, start the resize event watcher. - if isInteractive { - go sendResizeEvents(ctx, termFD, process) + if showInteractiveOutput { + go sendResizeEvents(ctx, outputFd, process) } go func() { @@ -373,17 +375,17 @@ func runCommand(ctx context.Context, client coder.Client, env *coder.Environment ap := activity.NewPusher(client, env.ID, sshActivityName) wr := ap.Writer(stdin) - if _, err := io.Copy(wr, os.Stdin); err != nil { + if _, err := io.Copy(wr, cmd.InOrStdin()); err != nil { cancel() } }() go func() { - if _, err := io.Copy(os.Stdout, process.Stdout()); err != nil { + if _, err := io.Copy(cmd.OutOrStdout(), process.Stdout()); err != nil { cancel() } }() go func() { - if _, err := io.Copy(os.Stderr, process.Stderr()); err != nil { + if _, err := io.Copy(cmd.ErrOrStderr(), process.Stderr()); err != nil { cancel() } }() diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go index c1c09bca..fc059ac0 100644 --- a/internal/cmd/sync.go +++ b/internal/cmd/sync.go @@ -90,11 +90,15 @@ func makeRunSync(init *bool) func(cmd *cobra.Command, args []string) error { } s := sync.Sync{ - Init: *init, - Env: *env, - RemoteDir: remoteDir, - LocalDir: absLocal, - Client: client, + Init: *init, + Env: *env, + RemoteDir: remoteDir, + LocalDir: absLocal, + Client: client, + OutW: cmd.OutOrStdout(), + ErrW: cmd.ErrOrStderr(), + InputReader: cmd.InOrStdin(), + IsInteractiveOutput: showInteractiveOutput, } localVersion := rsyncVersion() diff --git a/internal/cmd/tags.go b/internal/cmd/tags.go index 13163e60..3a357f14 100644 --- a/internal/cmd/tags.go +++ b/internal/cmd/tags.go @@ -2,7 +2,6 @@ package cmd import ( "encoding/json" - "os" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -113,7 +112,7 @@ func tagsLsCmd() *cobra.Command { return err } case jsonOutput: - err := json.NewEncoder(os.Stdout).Encode(tags) + err := json.NewEncoder(cmd.OutOrStdout()).Encode(tags) if err != nil { return err } diff --git a/internal/cmd/tokens.go b/internal/cmd/tokens.go index 66d11230..81a705c6 100644 --- a/internal/cmd/tokens.go +++ b/internal/cmd/tokens.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "fmt" - "os" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -56,7 +55,7 @@ func lsTokensCmd() *cobra.Command { return xerrors.Errorf("write table: %w", err) } case jsonOutput: - err := json.NewEncoder(os.Stdout).Encode(tokens) + err := json.NewEncoder(cmd.OutOrStdout()).Encode(tokens) if err != nil { return xerrors.Errorf("write tokens as JSON: %w", err) } diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index 8c3308ff..cb7f7ca1 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "os" "regexp" "strconv" "strings" @@ -107,7 +106,7 @@ func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) e return xerrors.Errorf("write table: %w", err) } case jsonOutput: - if err := json.NewEncoder(os.Stdout).Encode(devURLs); err != nil { + if err := json.NewEncoder(cmd.OutOrStdout()).Encode(devURLs); err != nil { return xerrors.Errorf("encode DevURLs as json: %w", err) } default: diff --git a/internal/cmd/users.go b/internal/cmd/users.go index a9d6725f..a844fd41 100644 --- a/internal/cmd/users.go +++ b/internal/cmd/users.go @@ -2,7 +2,6 @@ package cmd import ( "encoding/json" - "os" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -51,7 +50,7 @@ func listUsers(outputFmt *string) func(cmd *cobra.Command, args []string) error return xerrors.Errorf("write table: %w", err) } case "json": - if err := json.NewEncoder(os.Stdout).Encode(users); err != nil { + if err := json.NewEncoder(cmd.OutOrStdout()).Encode(users); err != nil { return xerrors.Errorf("encode users as json: %w", err) } default: diff --git a/internal/sync/sync.go b/internal/sync/sync.go index e9f16be8..cc6d22ea 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -41,8 +41,12 @@ type Sync struct { // DisableMetrics disables activity metric pushing. DisableMetrics bool - Env coder.Environment - Client coder.Client + Env coder.Environment + Client coder.Client + OutW io.Writer + ErrW io.Writer + InputReader io.Reader + IsInteractiveOutput bool } // See https://lxadm.com/Rsync_exit_codes#List_of_standard_rsync_exit_codes. @@ -71,9 +75,9 @@ func (s Sync) syncPaths(delete bool, local, remote string) error { // (AB): compression sped up the initial sync of the enterprise repo by 30%, leading me to believe it's // good in general for codebases. cmd := exec.Command("rsync", args...) - cmd.Stdout = os.Stdout + cmd.Stdout = s.OutW cmd.Stderr = ioutil.Discard - cmd.Stdin = os.Stdin + cmd.Stdin = s.InputReader if err := cmd.Run(); err != nil { if exitError, ok := err.(*exec.ExitError); ok { @@ -106,8 +110,8 @@ func (s Sync) remoteCmd(ctx context.Context, prog string, args ...string) error return xerrors.Errorf("exec remote process: %w", err) } // NOTE: If the copy routine fail, it will result in `process.Wait` to unblock and report an error. - go func() { _, _ = io.Copy(os.Stdout, process.Stdout()) }() // Best effort. - go func() { _, _ = io.Copy(os.Stderr, process.Stderr()) }() // Best effort. + go func() { _, _ = io.Copy(s.OutW, process.Stdout()) }() // Best effort. + go func() { _, _ = io.Copy(s.ErrW, process.Stderr()) }() // Best effort. if err := process.Wait(); err != nil { if code, ok := err.(wsep.ExitError); ok { @@ -235,7 +239,7 @@ func (s Sync) workEventGroup(evs []timedEvent) { var wg sync.WaitGroup for _, ev := range cache.ConcurrentEvents() { - setConsoleTitle(fmtUpdateTitle(ev.Path())) + setConsoleTitle(fmtUpdateTitle(ev.Path()), s.IsInteractiveOutput) wg.Add(1) // TODO: Document why this error is discarded. See https://github.com/cdr/coder-cli/issues/122 for reference. @@ -326,7 +330,7 @@ func (s Sync) Run() error { ap := activity.NewPusher(s.Client, s.Env.ID, activityName) ap.Push(ctx) - setConsoleTitle("⏳ syncing project") + setConsoleTitle("⏳ syncing project", s.IsInteractiveOutput) if err := s.initSync(); err != nil { return err } @@ -363,7 +367,7 @@ func (s Sync) Run() error { defer dispatchEventGroup.Stop() for { const watchingFilesystemTitle = "🛰 watching filesystem" - setConsoleTitle(watchingFilesystemTitle) + setConsoleTitle(watchingFilesystemTitle, s.IsInteractiveOutput) select { case ev := <-timedEvents: diff --git a/internal/sync/title.go b/internal/sync/title.go index c9a91c8b..ae7630d8 100644 --- a/internal/sync/title.go +++ b/internal/sync/title.go @@ -2,14 +2,11 @@ package sync import ( "fmt" - "os" "path/filepath" - - "golang.org/x/crypto/ssh/terminal" ) -func setConsoleTitle(title string) { - if !terminal.IsTerminal(int(os.Stdout.Fd())) { +func setConsoleTitle(title string, isInteractiveOutput bool) { + if !isInteractiveOutput { return } fmt.Printf("\033]0;%s\007", title) diff --git a/pkg/clog/clog.go b/pkg/clog/clog.go index eafc18aa..0a523e1f 100644 --- a/pkg/clog/clog.go +++ b/pkg/clog/clog.go @@ -3,6 +3,7 @@ package clog import ( "errors" "fmt" + "io" "os" "strings" @@ -10,6 +11,13 @@ import ( "golang.org/x/xerrors" ) +var writer io.Writer = os.Stderr + +// SetOutput sets the package-level writer target for log functions. +func SetOutput(w io.Writer) { + writer = w +} + // CLIMessage provides a human-readable message for CLI errors and messages. type CLIMessage struct { Level string @@ -45,12 +53,12 @@ func Log(err error) { if !xerrors.As(err, &cliErr) { cliErr = Fatal(err.Error()) } - fmt.Fprintln(os.Stderr, cliErr.String()) + fmt.Fprintln(writer, cliErr.String()) } // LogInfo prints the given info message to stderr. func LogInfo(header string, lines ...string) { - fmt.Fprint(os.Stderr, CLIMessage{ + fmt.Fprint(writer, CLIMessage{ Level: "info", Color: color.FgBlue, Header: header, @@ -60,7 +68,7 @@ func LogInfo(header string, lines ...string) { // LogSuccess prints the given info message to stderr. func LogSuccess(header string, lines ...string) { - fmt.Fprint(os.Stderr, CLIMessage{ + fmt.Fprint(writer, CLIMessage{ Level: "success", Color: color.FgGreen, Header: header, @@ -70,7 +78,7 @@ func LogSuccess(header string, lines ...string) { // LogWarn prints the given warn message to stderr. func LogWarn(header string, lines ...string) { - fmt.Fprint(os.Stderr, CLIMessage{ + fmt.Fprint(writer, CLIMessage{ Level: "warning", Color: color.FgYellow, Header: header, diff --git a/pkg/clog/clog_test.go b/pkg/clog/clog_test.go index 8d1c88b4..4c75a3e5 100644 --- a/pkg/clog/clog_test.go +++ b/pkg/clog/clog_test.go @@ -20,7 +20,7 @@ func TestError(t *testing.T) { assert.Success(t, "create pipe", err) //! clearly not thread safe - os.Stderr = writer + SetOutput(writer) Log(mockErr) writer.Close() @@ -39,7 +39,7 @@ func TestError(t *testing.T) { assert.Success(t, "create pipe", err) //! clearly not thread safe - os.Stderr = writer + SetOutput(writer) Log(mockErr) writer.Close() @@ -59,7 +59,7 @@ func TestError(t *testing.T) { assert.Success(t, "create pipe", err) //! clearly not thread safe - os.Stderr = writer + SetOutput(writer) Log(mockErr) writer.Close() From f34aafe57e79f7006ba8dc6e8bf6cb90bc9c8303 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 10 Mar 2021 14:40:52 -0600 Subject: [PATCH 2/6] Validate hostname, name, and cluster address on wsp create (#277) * Validate hostname, name, and cluster address on wsp create * fmt * comments --- coder-sdk/workspace_providers.go | 13 ++++++++++++- internal/cmd/providers.go | 24 +++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/coder-sdk/workspace_providers.go b/coder-sdk/workspace_providers.go index 33b94655..b252a1b9 100644 --- a/coder-sdk/workspace_providers.go +++ b/coder-sdk/workspace_providers.go @@ -41,6 +41,14 @@ const ( WorkspaceProviderReady WorkspaceProviderStatus = "ready" ) +// WorkspaceProviderType represents the type of workspace provider. +type WorkspaceProviderType string + +// Workspace Provider types. +const ( + WorkspaceProviderKubernetes WorkspaceProviderType = "kubernetes" +) + // WorkspaceProviderByID fetches a workspace provider entity by its unique ID. func (c *DefaultClient) WorkspaceProviderByID(ctx context.Context, id string) (*KubernetesProvider, error) { var wp KubernetesProvider @@ -63,7 +71,10 @@ func (c *DefaultClient) WorkspaceProviders(ctx context.Context) (*WorkspaceProvi // CreateWorkspaceProviderReq defines the request parameters for creating a new workspace provider entity. type CreateWorkspaceProviderReq struct { - Name string `json:"name"` + Name string `json:"name"` + Type WorkspaceProviderType `json:"type"` + Hostname string `json:"hostname"` + ClusterAddress string `json:"cluster_address"` } // CreateWorkspaceProviderRes defines the response from creating a new workspace provider entity. diff --git a/internal/cmd/providers.go b/internal/cmd/providers.go index e771dcf1..8f3530f6 100644 --- a/internal/cmd/providers.go +++ b/internal/cmd/providers.go @@ -3,11 +3,12 @@ package cmd import ( "fmt" + "cdr.dev/coder-cli/internal/x/xcobra" + "github.com/spf13/cobra" "golang.org/x/xerrors" "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/internal/x/xcobra" "cdr.dev/coder-cli/pkg/clog" "cdr.dev/coder-cli/pkg/tablewriter" ) @@ -29,13 +30,18 @@ func providersCmd() *cobra.Command { } func createProviderCmd() *cobra.Command { + var ( + hostname string + clusterAddress string + ) cmd := &cobra.Command{ - Use: "create [workspace_provider_name]", - Short: "create a new workspace provider.", + Use: "create [name] --hostname=[hostname] --clusterAddress=[clusterAddress]", Args: xcobra.ExactArgs(1), + Short: "create a new workspace provider.", Long: "Create a new Coder workspace provider.", Example: `# create a new workspace provider in a pending state -coder providers create my-new-workspace-provider`, + +coder providers create my-provider --hostname=https://provider.example.com --cluster-address=https://255.255.255.255`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -46,7 +52,10 @@ coder providers create my-new-workspace-provider`, // ExactArgs(1) ensures our name value can't panic on an out of bounds. createReq := &coder.CreateWorkspaceProviderReq{ - Name: args[0], + Name: args[0], + Type: coder.WorkspaceProviderKubernetes, + Hostname: hostname, + ClusterAddress: clusterAddress, } wp, err := client.CreateWorkspaceProvider(ctx, *createReq) @@ -63,6 +72,11 @@ coder providers create my-new-workspace-provider`, return nil }, } + + cmd.Flags().StringVar(&hostname, "hostname", "", "workspace provider hostname") + cmd.Flags().StringVar(&clusterAddress, "cluster-address", "", "kubernetes cluster apiserver endpoint") + _ = cmd.MarkFlagRequired("hostname") + _ = cmd.MarkFlagRequired("cluster-address") return cmd } From d604c27b4e1211a57c967e0afa98a31b31a9c7b4 Mon Sep 17 00:00:00 2001 From: Charles Moog Date: Wed, 10 Mar 2021 14:56:07 -0600 Subject: [PATCH 3/6] chore: simple no auth unit test (#279) --- internal/cmd/envs.go | 2 +- internal/cmd/envs_test.go | 76 +++++++++++++++++++++++++++++ internal/cmd/images.go | 2 +- internal/cmd/providers.go | 4 +- internal/cmd/tags.go | 2 +- internal/cmd/tokens.go | 2 +- internal/cmd/urls.go | 2 +- internal/cmd/users.go | 2 +- internal/config/dir.go | 11 +++-- pkg/tablewriter/tablewriter.go | 8 +-- pkg/tablewriter/tablewriter_test.go | 3 +- 11 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 internal/cmd/envs_test.go diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go index 73d71dd8..67739398 100644 --- a/internal/cmd/envs.go +++ b/internal/cmd/envs.go @@ -74,7 +74,7 @@ func lsEnvsCommand() *cobra.Command { switch outputFmt { case humanOutput: - err := tablewriter.WriteTable(len(envs), func(i int) interface{} { + err := tablewriter.WriteTable(cmd.OutOrStdout(), len(envs), func(i int) interface{} { return envs[i] }) if err != nil { diff --git a/internal/cmd/envs_test.go b/internal/cmd/envs_test.go new file mode 100644 index 00000000..879a20a0 --- /dev/null +++ b/internal/cmd/envs_test.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" + + "cdr.dev/coder-cli/internal/config" + "cdr.dev/coder-cli/pkg/clog" +) + +func init() { + tmpDir, err := ioutil.TempDir("", "coder-cli-config-dir") + if err != nil { + panic(err) + } + config.SetRoot(tmpDir) +} + +func TestEnvsCommand(t *testing.T) { + res := execute(t, []string{"envs", "ls"}, nil) + assert.Error(t, "execute without auth", res.ExitErr) + + err := assertClogErr(t, res.ExitErr) + assert.True(t, "login hint in error", strings.Contains(err.String(), "did you run \"coder login")) +} + +type result struct { + OutBuffer *bytes.Buffer + ErrBuffer *bytes.Buffer + ExitErr error +} + +func execute(t *testing.T, args []string, in io.Reader) result { + cmd := Make() + + outStream := bytes.NewBuffer(nil) + errStream := bytes.NewBuffer(nil) + + cmd.SetArgs(args) + + cmd.SetIn(in) + cmd.SetOut(outStream) + cmd.SetErr(errStream) + + err := cmd.Execute() + + slogtest.Debug(t, "execute command", + slog.F("outBuffer", outStream.String()), + slog.F("errBuffer", errStream.String()), + slog.F("args", args), + slog.F("execute_error", err), + ) + return result{ + OutBuffer: outStream, + ErrBuffer: errStream, + ExitErr: err, + } +} + +func assertClogErr(t *testing.T, err error) clog.CLIError { + var cliErr clog.CLIError + if !xerrors.As(err, &cliErr) { + slogtest.Fatal(t, "expected clog error, none found", slog.Error(err), slog.F("type", fmt.Sprintf("%T", err))) + } + slogtest.Debug(t, "clog error", slog.F("message", cliErr.String())) + return cliErr +} diff --git a/internal/cmd/images.go b/internal/cmd/images.go index ff47bf61..70364a59 100644 --- a/internal/cmd/images.go +++ b/internal/cmd/images.go @@ -70,7 +70,7 @@ func lsImgsCommand(user *string) *cobra.Command { } return nil case humanOutput: - err = tablewriter.WriteTable(len(imgs), func(i int) interface{} { + err = tablewriter.WriteTable(cmd.OutOrStdout(), len(imgs), func(i int) interface{} { return imgs[i] }) if err != nil { diff --git a/internal/cmd/providers.go b/internal/cmd/providers.go index 8f3530f6..aec63d3c 100644 --- a/internal/cmd/providers.go +++ b/internal/cmd/providers.go @@ -63,7 +63,7 @@ coder providers create my-provider --hostname=https://provider.example.com --clu return xerrors.Errorf("create workspace provider: %w", err) } - err = tablewriter.WriteTable(1, func(i int) interface{} { + err = tablewriter.WriteTable(cmd.OutOrStdout(), 1, func(i int) interface{} { return *wp }) if err != nil { @@ -100,7 +100,7 @@ coder providers ls`, return xerrors.Errorf("list workspace providers: %w", err) } - err = tablewriter.WriteTable(len(wps.Kubernetes), func(i int) interface{} { + err = tablewriter.WriteTable(cmd.OutOrStdout(), len(wps.Kubernetes), func(i int) interface{} { return wps.Kubernetes[i] }) if err != nil { diff --git a/internal/cmd/tags.go b/internal/cmd/tags.go index 3a357f14..91d7ba19 100644 --- a/internal/cmd/tags.go +++ b/internal/cmd/tags.go @@ -107,7 +107,7 @@ func tagsLsCmd() *cobra.Command { switch outputFmt { case humanOutput: - err = tablewriter.WriteTable(len(tags), func(i int) interface{} { return tags[i] }) + err = tablewriter.WriteTable(cmd.OutOrStdout(), len(tags), func(i int) interface{} { return tags[i] }) if err != nil { return err } diff --git a/internal/cmd/tokens.go b/internal/cmd/tokens.go index 81a705c6..21fd478f 100644 --- a/internal/cmd/tokens.go +++ b/internal/cmd/tokens.go @@ -48,7 +48,7 @@ func lsTokensCmd() *cobra.Command { switch outputFmt { case humanOutput: - err := tablewriter.WriteTable(len(tokens), func(i int) interface{} { + err := tablewriter.WriteTable(cmd.OutOrStdout(), len(tokens), func(i int) interface{} { return tokens[i] }) if err != nil { diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go index cb7f7ca1..587f843d 100644 --- a/internal/cmd/urls.go +++ b/internal/cmd/urls.go @@ -99,7 +99,7 @@ func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) e clog.LogInfo(fmt.Sprintf("no devURLs found for environment %q", envName)) return nil } - err := tablewriter.WriteTable(len(devURLs), func(i int) interface{} { + err := tablewriter.WriteTable(cmd.OutOrStdout(), len(devURLs), func(i int) interface{} { return devURLs[i] }) if err != nil { diff --git a/internal/cmd/users.go b/internal/cmd/users.go index a844fd41..03929366 100644 --- a/internal/cmd/users.go +++ b/internal/cmd/users.go @@ -46,7 +46,7 @@ func listUsers(outputFmt *string) func(cmd *cobra.Command, args []string) error case humanOutput: // For each element, return the user. each := func(i int) interface{} { return users[i] } - if err := tablewriter.WriteTable(len(users), each); err != nil { + if err := tablewriter.WriteTable(cmd.OutOrStdout(), len(users), each); err != nil { return xerrors.Errorf("write table: %w", err) } case "json": diff --git a/internal/config/dir.go b/internal/config/dir.go index 34cc7ab5..aff69fca 100644 --- a/internal/config/dir.go +++ b/internal/config/dir.go @@ -8,14 +8,17 @@ import ( "github.com/kirsle/configdir" ) -func dir() string { - return configdir.LocalConfig("coder") +var configRoot = configdir.LocalConfig("coder") + +// SetRoot overrides the package-level config root configuration. +func SetRoot(root string) { + configRoot = root } // open opens a file in the configuration directory, // creating all intermediate directories. func open(path string, flag int, mode os.FileMode) (*os.File, error) { - path = filepath.Join(dir(), path) + path = filepath.Join(configRoot, path) err := os.MkdirAll(filepath.Dir(path), 0750) if err != nil { @@ -45,5 +48,5 @@ func read(path string) ([]byte, error) { } func rm(path string) error { - return os.Remove(filepath.Join(dir(), path)) + return os.Remove(filepath.Join(configRoot, path)) } diff --git a/pkg/tablewriter/tablewriter.go b/pkg/tablewriter/tablewriter.go index 36a86bdf..9f12becd 100644 --- a/pkg/tablewriter/tablewriter.go +++ b/pkg/tablewriter/tablewriter.go @@ -3,7 +3,6 @@ package tablewriter import ( "fmt" "io" - "os" "reflect" "strings" "text/tabwriter" @@ -55,19 +54,16 @@ func StructFieldNames(data interface{}) string { return s.String() } -// The output io.Writer for WriteTable. This is globally defined to allow overriding in tests. -var tableOutput io.Writer = os.Stdout - // WriteTable writes the given list elements to stdout in a human readable // tabular format. Headers abide by the `table` struct tag. // // `table:"-"` omits the field and no tag defaults to the Go identifier. // `table:"_"` flattens a fields subfields. -func WriteTable(length int, each func(i int) interface{}) error { +func WriteTable(writer io.Writer, length int, each func(i int) interface{}) error { if length < 1 { return nil } - w := tabwriter.NewWriter(tableOutput, 0, 0, 4, ' ', 0) + w := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0) defer func() { _ = w.Flush() }() // Best effort. for ix := 0; ix < length; ix++ { item := each(ix) diff --git a/pkg/tablewriter/tablewriter_test.go b/pkg/tablewriter/tablewriter_test.go index 5318dd1e..e611e52c 100644 --- a/pkg/tablewriter/tablewriter_test.go +++ b/pkg/tablewriter/tablewriter_test.go @@ -49,8 +49,7 @@ func TestTableWriter(t *testing.T) { } buf := bytes.NewBuffer(nil) - tableOutput = buf - err := WriteTable(len(items), func(i int) interface{} { return items[i] }) + err := WriteTable(buf, len(items), func(i int) interface{} { return items[i] }) assert.Success(t, "write table", err) assertGolden(t, "table_output.golden", buf.Bytes()) From dc4426218190bc538e7b0abf8632cb33b549e026 Mon Sep 17 00:00:00 2001 From: Charles Moog Date: Wed, 10 Mar 2021 16:24:47 -0600 Subject: [PATCH 4/6] chore: add auth init to unit tests (#280) --- .github/workflows/test.yaml | 3 + internal/cmd/cli_test.go | 142 +++++++++++++++++++++++++++++++++ internal/cmd/envs_test.go | 74 ++--------------- internal/cmd/providers_test.go | 10 +++ 4 files changed, 163 insertions(+), 66 deletions(-) create mode 100644 internal/cmd/cli_test.go create mode 100644 internal/cmd/providers_test.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 45ac921d..9a3261ea 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -40,6 +40,9 @@ jobs: uses: ./ci/image env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CODER_URL: ${{ secrets.CODER_URL }} + CODER_EMAIL: ${{ secrets.CODER_EMAIL }} + CODER_PASSWORD: ${{ secrets.CODER_PASSWORD }} with: args: make -j test/coverage gendocs: diff --git a/internal/cmd/cli_test.go b/internal/cmd/cli_test.go new file mode 100644 index 00000000..d1c639dc --- /dev/null +++ b/internal/cmd/cli_test.go @@ -0,0 +1,142 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "strings" + "testing" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/internal/config" + "cdr.dev/coder-cli/pkg/clog" +) + +func init() { + tmpDir, err := ioutil.TempDir("", "coder-cli-config-dir") + if err != nil { + panic(err) + } + config.SetRoot(tmpDir) + + // TODO: might need to make this a command scoped option to make assertions against its output + clog.SetOutput(ioutil.Discard) + + email := os.Getenv("CODER_EMAIL") + password := os.Getenv("CODER_PASSWORD") + rawURL := os.Getenv("CODER_URL") + if email == "" || password == "" || rawURL == "" { + panic("CODER_EMAIL, CODER_PASSWORD, and CODER_URL are required environment variables") + } + u, err := url.Parse(rawURL) + if err != nil { + panic("invalid CODER_URL: " + err.Error()) + } + client, err := coder.NewClient(coder.ClientOptions{ + BaseURL: u, + Email: email, + Password: password, + }) + if err != nil { + panic("new client: " + err.Error()) + } + if err := config.URL.Write(rawURL); err != nil { + panic("write config url: " + err.Error()) + } + if err := config.Session.Write(client.Token()); err != nil { + panic("write config token: " + err.Error()) + } +} + +type result struct { + outBuffer *bytes.Buffer + errBuffer *bytes.Buffer + exitErr error +} + +func (r result) success(t *testing.T) { + t.Helper() + assert.Success(t, "execute command", r.exitErr) +} + +//nolint +func (r result) stdoutContains(t *testing.T, substring string) { + t.Helper() + if !strings.Contains(r.outBuffer.String(), substring) { + slogtest.Fatal(t, "stdout contains substring", slog.F("substring", substring), slog.F("stdout", r.outBuffer.String())) + } +} + +//nolint +func (r result) stdoutUnmarshals(t *testing.T, target interface{}) { + t.Helper() + err := json.Unmarshal(r.outBuffer.Bytes(), target) + assert.Success(t, "unmarshal json", err) +} + +//nolint +func (r result) stdoutEmpty(t *testing.T) { + t.Helper() + assert.Equal(t, "stdout empty", "", r.outBuffer.String()) +} + +//nolint +func (r result) stderrEmpty(t *testing.T) { + t.Helper() + assert.Equal(t, "stderr empty", "", r.errBuffer.String()) +} + +//nolint +func (r result) stderrContains(t *testing.T, substring string) { + t.Helper() + if !strings.Contains(r.errBuffer.String(), substring) { + slogtest.Fatal(t, "stderr contains substring", slog.F("substring", substring), slog.F("stderr", r.errBuffer.String())) + } +} + +//nolint +func (r result) clogError(t *testing.T) clog.CLIError { + t.Helper() + var cliErr clog.CLIError + if !xerrors.As(r.exitErr, &cliErr) { + slogtest.Fatal(t, "expected clog error, none found", slog.Error(r.exitErr), slog.F("type", fmt.Sprintf("%T", r.exitErr))) + } + slogtest.Debug(t, "clog error", slog.F("message", cliErr.String())) + return cliErr +} + +func execute(t *testing.T, in io.Reader, args ...string) result { + cmd := Make() + + var outStream bytes.Buffer + var errStream bytes.Buffer + + cmd.SetArgs(args) + + cmd.SetIn(in) + cmd.SetOut(&outStream) + cmd.SetErr(&errStream) + + err := cmd.Execute() + + slogtest.Debug(t, "execute command", + slog.F("out_buffer", outStream.String()), + slog.F("err_buffer", errStream.String()), + slog.F("args", args), + slog.F("execute_error", err), + ) + return result{ + outBuffer: &outStream, + errBuffer: &errStream, + exitErr: err, + } +} diff --git a/internal/cmd/envs_test.go b/internal/cmd/envs_test.go index 879a20a0..c7cc6451 100644 --- a/internal/cmd/envs_test.go +++ b/internal/cmd/envs_test.go @@ -1,76 +1,18 @@ package cmd import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "strings" "testing" - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - "cdr.dev/slog/sloggers/slogtest/assert" - "golang.org/x/xerrors" - - "cdr.dev/coder-cli/internal/config" - "cdr.dev/coder-cli/pkg/clog" + "cdr.dev/coder-cli/coder-sdk" ) -func init() { - tmpDir, err := ioutil.TempDir("", "coder-cli-config-dir") - if err != nil { - panic(err) - } - config.SetRoot(tmpDir) -} - -func TestEnvsCommand(t *testing.T) { - res := execute(t, []string{"envs", "ls"}, nil) - assert.Error(t, "execute without auth", res.ExitErr) - - err := assertClogErr(t, res.ExitErr) - assert.True(t, "login hint in error", strings.Contains(err.String(), "did you run \"coder login")) -} - -type result struct { - OutBuffer *bytes.Buffer - ErrBuffer *bytes.Buffer - ExitErr error -} +func Test_envs_ls(t *testing.T) { + res := execute(t, nil, "envs", "ls") + res.success(t) -func execute(t *testing.T, args []string, in io.Reader) result { - cmd := Make() - - outStream := bytes.NewBuffer(nil) - errStream := bytes.NewBuffer(nil) - - cmd.SetArgs(args) - - cmd.SetIn(in) - cmd.SetOut(outStream) - cmd.SetErr(errStream) - - err := cmd.Execute() - - slogtest.Debug(t, "execute command", - slog.F("outBuffer", outStream.String()), - slog.F("errBuffer", errStream.String()), - slog.F("args", args), - slog.F("execute_error", err), - ) - return result{ - OutBuffer: outStream, - ErrBuffer: errStream, - ExitErr: err, - } -} + res = execute(t, nil, "envs", "ls", "--output=json") + res.success(t) -func assertClogErr(t *testing.T, err error) clog.CLIError { - var cliErr clog.CLIError - if !xerrors.As(err, &cliErr) { - slogtest.Fatal(t, "expected clog error, none found", slog.Error(err), slog.F("type", fmt.Sprintf("%T", err))) - } - slogtest.Debug(t, "clog error", slog.F("message", cliErr.String())) - return cliErr + var envs []coder.Environment + res.stdoutUnmarshals(t, &envs) } diff --git a/internal/cmd/providers_test.go b/internal/cmd/providers_test.go new file mode 100644 index 00000000..14900759 --- /dev/null +++ b/internal/cmd/providers_test.go @@ -0,0 +1,10 @@ +package cmd + +import ( + "testing" +) + +func Test_providers_ls(t *testing.T) { + res := execute(t, nil, "providers", "ls") + res.success(t) +} From 94a596daa64970ad846d3542ca0dbc83b19ce597 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Mar 2021 15:35:17 -0800 Subject: [PATCH 5/6] Bump github.com/manifoldco/promptui from 0.7.0 to 0.8.0 (#265) Bumps [github.com/manifoldco/promptui](https://github.com/manifoldco/promptui) from 0.7.0 to 0.8.0. - [Release notes](https://github.com/manifoldco/promptui/releases) - [Changelog](https://github.com/manifoldco/promptui/blob/master/CHANGELOG.md) - [Commits](https://github.com/manifoldco/promptui/compare/v0.7.0...v0.8.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 34863a2a..a2ae1c46 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/klauspost/compress v1.10.8 // indirect - github.com/manifoldco/promptui v0.7.0 + github.com/manifoldco/promptui v0.8.0 github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.1.1 diff --git a/go.sum b/go.sum index d1a98db5..c4cc5343 100644 --- a/go.sum +++ b/go.sum @@ -197,8 +197,8 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= -github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= +github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= From bd1493be53891e49f1a295428a84e913f146416b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Mar 2021 23:51:53 +0000 Subject: [PATCH 6/6] Bump github.com/google/go-cmp from 0.4.0 to 0.5.5 Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.4.0 to 0.5.5. - [Release notes](https://github.com/google/go-cmp/releases) - [Commits](https://github.com/google/go-cmp/compare/v0.4.0...v0.5.5) Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a2ae1c46..585b808e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( cdr.dev/wsep v0.0.0-20200728013649-82316a09813f github.com/briandowns/spinner v1.11.1 github.com/fatih/color v1.10.0 - github.com/google/go-cmp v0.4.0 + github.com/google/go-cmp v0.5.5 github.com/gorilla/websocket v1.4.2 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/klauspost/compress v1.10.8 // indirect diff --git a/go.sum b/go.sum index c4cc5343..972c8f3b 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=