From d6a1eb825d3d02695f30eb367e3059e78c8b28d5 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 9 Feb 2022 19:00:40 +0000 Subject: [PATCH 1/7] feat: Add "coder" CLI --- .vscode/settings.json | 3 + cli/config/file.go | 71 ++++++++++++++++++ cli/config/file_test.go | 38 ++++++++++ cli/login.go | 131 ++++++++++++++++++++++++++++++++ cli/projectcreate.go | 157 +++++++++++++++++++++++++++++++++++++++ cli/projectplan.go | 16 ++++ cli/projects.go | 30 ++++++++ cli/projectupdate.go | 14 ++++ cli/root.go | 161 ++++++++++++++++++++++++++++++++++++++++ cli/ssh.go | 1 + cli/users.go | 10 +++ cli/workspaces.go | 11 +++ cmd/coder/main.go | 11 ++- coderd/cmd/root.go | 53 ++++++++++++- coderd/coderd.go | 1 + coderd/users.go | 20 +++++ coderd/users_test.go | 20 +++++ codersdk/users.go | 17 +++++ codersdk/users_test.go | 20 +++++ go.mod | 8 +- go.sum | 10 +++ 21 files changed, 798 insertions(+), 5 deletions(-) create mode 100644 cli/config/file.go create mode 100644 cli/config/file_test.go create mode 100644 cli/login.go create mode 100644 cli/projectcreate.go create mode 100644 cli/projectplan.go create mode 100644 cli/projects.go create mode 100644 cli/projectupdate.go create mode 100644 cli/root.go create mode 100644 cli/ssh.go create mode 100644 cli/users.go create mode 100644 cli/workspaces.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 3b78316937691..3178a1698bd5d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,16 +31,19 @@ "drpcconn", "drpcmux", "drpcserver", + "fatih", "goleak", "hashicorp", "httpmw", "Jobf", + "manifoldco", "moby", "nhooyr", "nolint", "nosec", "oneof", "parameterscopeid", + "promptui", "protobuf", "provisionerd", "provisionersdk", diff --git a/cli/config/file.go b/cli/config/file.go new file mode 100644 index 0000000000000..45f70e580c0ac --- /dev/null +++ b/cli/config/file.go @@ -0,0 +1,71 @@ +package config + +import ( + "io/ioutil" + "os" + "path/filepath" +) + +// Root represents the configuration directory. +type Root string + +func (r Root) Session() File { + return File(filepath.Join(string(r), "session")) +} + +func (r Root) URL() File { + return File(filepath.Join(string(r), "url")) +} + +func (r Root) Organization() File { + return File(filepath.Join(string(r), "organization")) +} + +// File provides convenience methods for interacting with *os.File. +type File string + +// Delete deletes the file. +func (f File) Delete() error { + return os.Remove(string(f)) +} + +// Write writes the string to the file. +func (f File) Write(s string) error { + return write(string(f), 0600, []byte(s)) +} + +// Read reads the file to a string. +func (f File) Read() (string, error) { + byt, err := read(string(f)) + return string(byt), err +} + +// open opens a file in the configuration directory, +// creating all intermediate directories. +func open(path string, flag int, mode os.FileMode) (*os.File, error) { + err := os.MkdirAll(filepath.Dir(path), 0750) + if err != nil { + return nil, err + } + + return os.OpenFile(path, flag, mode) +} + +func write(path string, mode os.FileMode, dat []byte) error { + fi, err := open(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode) + if err != nil { + return err + } + defer fi.Close() + _, err = fi.Write(dat) + return err +} + +func read(path string) ([]byte, error) { + fi, err := open(path, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + defer fi.Close() + return ioutil.ReadAll(fi) +} diff --git a/cli/config/file_test.go b/cli/config/file_test.go new file mode 100644 index 0000000000000..7e634ac08b420 --- /dev/null +++ b/cli/config/file_test.go @@ -0,0 +1,38 @@ +package config_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/cli/config" +) + +func TestFile(t *testing.T) { + t.Parallel() + + t.Run("Write", func(t *testing.T) { + t.Parallel() + err := config.Root(t.TempDir()).Session().Write("test") + require.NoError(t, err) + }) + + t.Run("Read", func(t *testing.T) { + t.Parallel() + root := config.Root(t.TempDir()) + err := root.Session().Write("test") + require.NoError(t, err) + data, err := root.Session().Read() + require.NoError(t, err) + require.Equal(t, "test", string(data)) + }) + + t.Run("Delete", func(t *testing.T) { + t.Parallel() + root := config.Root(t.TempDir()) + err := root.Session().Write("test") + require.NoError(t, err) + err = root.Session().Delete() + require.NoError(t, err) + }) +} diff --git a/cli/login.go b/cli/login.go new file mode 100644 index 0000000000000..6edec77d74027 --- /dev/null +++ b/cli/login.go @@ -0,0 +1,131 @@ +package cli + +import ( + "errors" + "fmt" + "net/url" + "strings" + + "github.com/coder/coder/coderd" + "github.com/coder/coder/codersdk" + "github.com/fatih/color" + "github.com/go-playground/validator/v10" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +func login() *cobra.Command { + return &cobra.Command{ + Use: "login ", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + rawURL := args[0] + if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { + scheme := "https" + if strings.HasPrefix(rawURL, "localhost") { + scheme = "http" + } + rawURL = fmt.Sprintf("%s://%s", scheme, rawURL) + } + serverURL, err := url.Parse(rawURL) + if err != nil { + return err + } + // Default to HTTPs. Enables simple URLs like: master.cdr.dev + if serverURL.Scheme == "" { + serverURL.Scheme = "https" + } + + client := codersdk.New(serverURL) + hasInitialUser, err := client.HasInitialUser(cmd.Context()) + if err != nil { + return err + } + if !hasInitialUser { + if !isTTY(cmd.InOrStdin()) { + return xerrors.New("the initial user cannot be created in non-interactive mode. use the API") + } + fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been setup!\n", color.HiBlackString(">")) + + _, err := runPrompt(cmd, &promptui.Prompt{ + Label: "Would you like to create the first user?", + IsConfirm: true, + Default: "y", + }) + if err != nil { + return err + } + + username, err := runPrompt(cmd, &promptui.Prompt{ + Label: "What username would you like?", + Default: "kyle", + }) + if err != nil { + return err + } + + organization, err := runPrompt(cmd, &promptui.Prompt{ + Label: "What is the name of your organization?", + Default: "acme-corp", + }) + if err != nil { + return err + } + + email, err := runPrompt(cmd, &promptui.Prompt{ + Label: "What's your email?", + Validate: func(s string) error { + err := validator.New().Var(s, "email") + if err != nil { + return errors.New("That's not a valid email address!") + } + return err + }, + }) + if err != nil { + return err + } + + password, err := runPrompt(cmd, &promptui.Prompt{ + Label: "Enter a password:", + Mask: '*', + }) + if err != nil { + return err + } + + _, err = client.CreateInitialUser(cmd.Context(), coderd.CreateInitialUserRequest{ + Email: email, + Username: username, + Password: password, + Organization: organization, + }) + if err != nil { + return xerrors.Errorf("create initial user: %w", err) + } + resp, err := client.LoginWithPassword(cmd.Context(), coderd.LoginWithPasswordRequest{ + Email: email, + Password: password, + }) + if err != nil { + return xerrors.Errorf("login with password: %w", err) + } + config := createConfig(cmd) + err = config.Session().Write(resp.SessionToken) + if err != nil { + return xerrors.Errorf("write session token: %w", err) + } + err = config.URL().Write(serverURL.String()) + if err != nil { + return xerrors.Errorf("write server url: %w", err) + } + + fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're logged in.\n", color.HiBlackString(">"), color.HiCyanString(username)) + return nil + } + + return nil + }, + } +} diff --git a/cli/projectcreate.go b/cli/projectcreate.go new file mode 100644 index 0000000000000..5d8d000971456 --- /dev/null +++ b/cli/projectcreate.go @@ -0,0 +1,157 @@ +package cli + +import ( + "archive/tar" + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/briandowns/spinner" + "github.com/coder/coder/coderd" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/database" + "github.com/fatih/color" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "golang.org/x/xerrors" +) + +func projectCreate() *cobra.Command { + return &cobra.Command{ + Use: "create", + Short: "Create a project from the current directory", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := createClient(cmd) + if err != nil { + return err + } + organization, err := currentOrganization(cmd, client) + if err != nil { + return err + } + + workingDir, err := os.Getwd() + if err != nil { + return err + } + + _, err = runPrompt(cmd, &promptui.Prompt{ + Default: "y", + IsConfirm: true, + Label: fmt.Sprintf("Set up %s in your organization?", color.New(color.FgHiCyan).Sprintf("%q", workingDir)), + }) + if err != nil { + return err + } + + name := filepath.Base(workingDir) + name, err = runPrompt(cmd, &promptui.Prompt{ + Default: name, + Label: "What's your project's name?", + Validate: func(s string) error { + _, err = client.Project(cmd.Context(), organization.Name, s) + if err == nil { + return xerrors.New("A project already exists with that name!") + } + return nil + }, + }) + if err != nil { + return err + } + + spin := spinner.New(spinner.CharSets[0], 50*time.Millisecond) + spin.Suffix = " Uploading current directory..." + spin.Start() + defer spin.Stop() + + bytes, err := tarDir(workingDir) + if err != nil { + return err + } + + resp, err := client.UploadFile(cmd.Context(), codersdk.ContentTypeTar, bytes) + if err != nil { + return err + } + + job, err := client.CreateProjectVersionImportProvisionerJob(cmd.Context(), organization.Name, coderd.CreateProjectImportJobRequest{ + StorageMethod: database.ProvisionerStorageMethodFile, + StorageSource: resp.Hash, + Provisioner: database.ProvisionerTypeTerraform, + // SkipResources on first import to detect variables defined by the project. + SkipResources: true, + }) + if err != nil { + return err + } + spin.Stop() + + time.Sleep(time.Second) + + logs, err := client.FollowProvisionerJobLogsAfter(context.Background(), organization.Name, job.ID, time.Time{}) + if err != nil { + return err + } + for { + log, ok := <-logs + if !ok { + break + } + fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[parse]"), log.Output) + } + + fmt.Printf("Projects %+v %+v\n", projects, organization) + return nil + }, + } +} + +func tarDir(directory string) ([]byte, error) { + var buffer bytes.Buffer + tw := tar.NewWriter(&buffer) + // walk through every file in the folder + err := filepath.Walk(directory, func(file string, fi os.FileInfo, err error) error { + // generate tar header + header, err := tar.FileInfoHeader(fi, file) + if err != nil { + return err + } + + // must provide real name + // (see https://golang.org/src/archive/tar/common.go?#L626) + rel, err := filepath.Rel(directory, file) + if err != nil { + return err + } + header.Name = rel + + // write header + if err := tw.WriteHeader(header); err != nil { + return err + } + // if not a dir, write file content + if !fi.IsDir() { + data, err := os.Open(file) + if err != nil { + return err + } + if _, err := io.Copy(tw, data); err != nil { + return err + } + } + return nil + }) + if err != nil { + return nil, err + } + err = tw.Flush() + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} diff --git a/cli/projectplan.go b/cli/projectplan.go new file mode 100644 index 0000000000000..963c02d975cb1 --- /dev/null +++ b/cli/projectplan.go @@ -0,0 +1,16 @@ +package cli + +import ( + "github.com/spf13/cobra" +) + +func projectPlan() *cobra.Command { + return &cobra.Command{ + Use: "plan ", + Args: cobra.MinimumNArgs(1), + Short: "Plan a project update from the current directory", + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + } +} diff --git a/cli/projects.go b/cli/projects.go new file mode 100644 index 0000000000000..07d68c0155967 --- /dev/null +++ b/cli/projects.go @@ -0,0 +1,30 @@ +package cli + +import ( + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +func projects() *cobra.Command { + cmd := &cobra.Command{ + Use: "projects", + Long: "Testing something", + Example: ` + - Create a project for developers to create workspaces + + ` + color.New(color.FgHiMagenta).Sprint("$ coder projects create") + ` + + - Make changes to your project, and plan the changes + + ` + color.New(color.FgHiMagenta).Sprint("$ coder projects plan ") + ` + + - Update the project. Your developers can update their workspaces + + ` + color.New(color.FgHiMagenta).Sprint("$ coder projects update "), + } + cmd.AddCommand(projectCreate()) + cmd.AddCommand(projectPlan()) + cmd.AddCommand(projectUpdate()) + + return cmd +} diff --git a/cli/projectupdate.go b/cli/projectupdate.go new file mode 100644 index 0000000000000..19f7b2b06404f --- /dev/null +++ b/cli/projectupdate.go @@ -0,0 +1,14 @@ +package cli + +import "github.com/spf13/cobra" + +func projectUpdate() *cobra.Command { + return &cobra.Command{ + Use: "update ", + Args: cobra.MinimumNArgs(1), + Short: "Update a project from the current directory", + RunE: func(cmd *cobra.Command, args []string) error { + return nil + }, + } +} diff --git a/cli/root.go b/cli/root.go new file mode 100644 index 0000000000000..95a27116db80d --- /dev/null +++ b/cli/root.go @@ -0,0 +1,161 @@ +package cli + +import ( + "fmt" + "io" + "net/url" + "os" + "strings" + + "github.com/coder/coder/cli/config" + "github.com/coder/coder/coderd" + "github.com/coder/coder/codersdk" + "github.com/fatih/color" + "github.com/kirsle/configdir" + "github.com/manifoldco/promptui" + "github.com/mattn/go-isatty" + "github.com/spf13/cobra" +) + +const ( + varGlobalConfig = "global-config" +) + +func Root() *cobra.Command { + cmd := &cobra.Command{ + Use: "coder", + Long: ` ▄█▀ ▀█▄ + ▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█ + ▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀ +█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █ + ██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀ + ` + color.New(color.Underline).Sprint("Self-hosted developer workspaces on your infra") + ` + +`, + Example: ` + - Create a project for developers to create workspaces + + ` + color.New(color.FgHiMagenta).Sprint("$ coder projects create ") + ` + + - Create a workspace for a specific project + + ` + color.New(color.FgHiMagenta).Sprint("$ coder workspaces create ") + ` + + - Maintain consistency by updating a workspace + + ` + color.New(color.FgHiMagenta).Sprint("$ coder workspaces update "), + } + // Customizes the color of headings to make subcommands + // more visually appealing. + header := color.New(color.FgHiBlack) + cmd.SetUsageTemplate(strings.NewReplacer( + `Usage:`, header.Sprint("Usage:"), + `Examples:`, header.Sprint("Examples:"), + `Available Commands:`, header.Sprint("Commands:"), + `Global Flags:`, header.Sprint("Global Flags:"), + `Flags:`, header.Sprint("Flags:"), + `Additional help topics:`, header.Sprint("Additional help:"), + ).Replace(cmd.UsageTemplate())) + + cmd.AddCommand(login()) + cmd.AddCommand(projects()) + cmd.AddCommand(workspaces()) + cmd.AddCommand(users()) + + cmd.PersistentFlags().String(varGlobalConfig, configdir.LocalConfig("coder"), "Path to the global `coder` config directory") + + return cmd +} + +func createClient(cmd *cobra.Command) (*codersdk.Client, error) { + root := createConfig(cmd) + rawURL, err := root.URL().Read() + if err != nil { + return nil, err + } + serverURL, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + token, err := root.Session().Read() + if err != nil { + return nil, err + } + client := codersdk.New(serverURL) + return client, client.SetSessionToken(token) +} + +func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (coderd.Organization, error) { + orgs, err := client.UserOrganizations(cmd.Context(), "me") + if err != nil { + return coderd.Organization{}, nil + } + // For now, we won't use the config to set this. + // Eventually, we will support changing using "coder switch " + return orgs[0], nil +} + +func createConfig(cmd *cobra.Command) config.Root { + globalRoot, err := cmd.Flags().GetString(varGlobalConfig) + if err != nil { + panic(err) + } + return config.Root(globalRoot) +} + +// isTTY returns whether the passed reader is a TTY or not. +// This accepts a reader to work with Cobra's "InOrStdin" +// function for simple testing. +func isTTY(reader io.Reader) bool { + file, ok := reader.(*os.File) + if !ok { + return false + } + return isatty.IsTerminal(file.Fd()) +} + +func runPrompt(cmd *cobra.Command, prompt *promptui.Prompt) (string, error) { + prompt.Stdin = cmd.InOrStdin().(io.ReadCloser) + prompt.Stdout = cmd.OutOrStdout().(io.WriteCloser) + + // The prompt library displays defaults in a jarring way for the user + // by attempting to autocomplete it. This sets no default enabling us + // to customize the display. + defaultValue := prompt.Default + if !prompt.IsConfirm { + prompt.Default = "" + } + + // Rewrite the confirm template to remove bold, and fit to the Coder style. + confirmEnd := fmt.Sprintf("[y/%s] ", color.New(color.Bold).Sprint("N")) + if prompt.Default == "y" { + confirmEnd = fmt.Sprintf("[%s/n] ", color.New(color.Bold).Sprint("Y")) + } + confirm := color.HiBlackString("?") + ` {{ . }} ` + confirmEnd + + // Customize to remove bold. + valid := color.HiBlackString("?") + " {{ . }} " + if defaultValue != "" { + valid += fmt.Sprintf("(%s) ", defaultValue) + } + + success := valid + invalid := valid + if prompt.IsConfirm { + success = confirm + invalid = confirm + } + + prompt.Templates = &promptui.PromptTemplates{ + Confirm: confirm, + Success: success, + Invalid: invalid, + Valid: valid, + } + value, err := prompt.Run() + if value == "" && !prompt.IsConfirm { + value = defaultValue + } + + return value, err +} diff --git a/cli/ssh.go b/cli/ssh.go new file mode 100644 index 0000000000000..7f1e458cd3abe --- /dev/null +++ b/cli/ssh.go @@ -0,0 +1 @@ +package cli diff --git a/cli/users.go b/cli/users.go new file mode 100644 index 0000000000000..7dd3f309d44b3 --- /dev/null +++ b/cli/users.go @@ -0,0 +1,10 @@ +package cli + +import "github.com/spf13/cobra" + +func users() *cobra.Command { + cmd := &cobra.Command{ + Use: "users", + } + return cmd +} diff --git a/cli/workspaces.go b/cli/workspaces.go new file mode 100644 index 0000000000000..4140d8c9ed7a2 --- /dev/null +++ b/cli/workspaces.go @@ -0,0 +1,11 @@ +package cli + +import "github.com/spf13/cobra" + +func workspaces() *cobra.Command { + cmd := &cobra.Command{ + Use: "workspaces", + } + + return cmd +} diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 741337dc0372d..3daffadbb921d 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -1,7 +1,14 @@ package main -import "fmt" +import ( + "os" + + "github.com/coder/coder/cli" +) func main() { - _, _ = fmt.Println("Hello World!") + err := cli.Root().Execute() + if err != nil { + os.Exit(1) + } } diff --git a/coderd/cmd/root.go b/coderd/cmd/root.go index e63f4a50a901c..4c3f0d602816d 100644 --- a/coderd/cmd/root.go +++ b/coderd/cmd/root.go @@ -1,9 +1,15 @@ package cmd import ( + "context" + "fmt" + "io" + "io/ioutil" "net" "net/http" + "net/url" "os" + "time" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -11,8 +17,13 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/coderd" + "github.com/coder/coder/codersdk" "github.com/coder/coder/database" "github.com/coder/coder/database/databasefake" + "github.com/coder/coder/provisioner/terraform" + "github.com/coder/coder/provisionerd" + "github.com/coder/coder/provisionersdk" + "github.com/coder/coder/provisionersdk/proto" ) func Root() *cobra.Command { @@ -22,8 +33,9 @@ func Root() *cobra.Command { root := &cobra.Command{ Use: "coderd", RunE: func(cmd *cobra.Command, args []string) error { + logger := slog.Make(sloghuman.Sink(os.Stderr)) handler := coderd.New(&coderd.Options{ - Logger: slog.Make(sloghuman.Sink(os.Stderr)), + Logger: logger, Database: databasefake.New(), Pubsub: database.NewPubsubInMemory(), }) @@ -34,6 +46,17 @@ func Root() *cobra.Command { } defer listener.Close() + serverURL, err := url.Parse(fmt.Sprintf("http://%s", address)) + if err != nil { + return xerrors.Errorf("parse %q: %w", address, err) + } + client := codersdk.New(serverURL) + closer, err := newProvisionerDaemon(cmd.Context(), client, logger) + if err != nil { + return xerrors.Errorf("create provisioner daemon: %w", err) + } + defer closer.Close() + errCh := make(chan error) go func() { defer close(errCh) @@ -56,3 +79,31 @@ func Root() *cobra.Command { return root } + +func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger) (io.Closer, error) { + terraformClient, terraformServer := provisionersdk.TransportPipe() + go func() { + err := terraform.Serve(ctx, &terraform.ServeOptions{ + ServeOptions: &provisionersdk.ServeOptions{ + Listener: terraformServer, + }, + Logger: logger, + }) + if err != nil { + panic(err) + } + }() + tempDir, err := ioutil.TempDir("", "provisionerd") + if err != nil { + return nil, err + } + return provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{ + Logger: logger, + PollInterval: 50 * time.Millisecond, + UpdateInterval: 50 * time.Millisecond, + Provisioners: provisionerd.Provisioners{ + string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(provisionersdk.Conn(terraformClient)), + }, + WorkDirectory: tempDir, + }), nil +} diff --git a/coderd/coderd.go b/coderd/coderd.go index 82486b98722ef..b609ceaec1385 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -37,6 +37,7 @@ func New(options *Options) http.Handler { r.Post("/login", api.postLogin) r.Post("/logout", api.postLogout) // Used for setup. + r.Get("/user", api.user) r.Post("/user", api.postUser) r.Route("/users", func(r chi.Router) { r.Use( diff --git a/coderd/users.go b/coderd/users.go index 0644a78d01aff..ab6328fbe1984 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -55,6 +55,26 @@ type LoginWithPasswordResponse struct { SessionToken string `json:"session_token" validate:"required"` } +// Returns whether the initial user has been created or not. +func (api *api) user(rw http.ResponseWriter, r *http.Request) { + userCount, err := api.Database.GetUserCount(r.Context()) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get user count: %s", err.Error()), + }) + return + } + if userCount == 0 { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "The initial user has not been created!", + }) + return + } + httpapi.Write(rw, http.StatusOK, httpapi.Response{ + Message: "The initial user has already been created!", + }) +} + // Creates the initial user for a Coder deployment. func (api *api) postUser(rw http.ResponseWriter, r *http.Request) { var createUser CreateInitialUserRequest diff --git a/coderd/users_test.go b/coderd/users_test.go index b3f36b3dd0914..8ef1464856f75 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -13,6 +13,26 @@ import ( "github.com/coder/coder/httpmw" ) +func TestUser(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + has, err := client.HasInitialUser(context.Background()) + require.NoError(t, err) + require.False(t, has) + }) + + t.Run("Found", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + has, err := client.HasInitialUser(context.Background()) + require.NoError(t, err) + require.True(t, has) + }) +} + func TestPostUser(t *testing.T) { t.Parallel() t.Run("BadRequest", func(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index c3a8c13b847ee..b17b5a9931e6e 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -9,6 +9,23 @@ import ( "github.com/coder/coder/coderd" ) +// HasInitialUser returns whether the initial user has already been +// created or not. +func (c *Client) HasInitialUser(ctx context.Context) (bool, error) { + res, err := c.request(ctx, http.MethodGet, "/api/v2/user", nil) + if err != nil { + return false, err + } + defer res.Body.Close() + if res.StatusCode == http.StatusNotFound { + return false, nil + } + if res.StatusCode != http.StatusOK { + return false, readBodyAsError(res) + } + return true, nil +} + // CreateInitialUser attempts to create the first user on a Coder deployment. // This initial user has superadmin privileges. If >0 users exist, this request // will fail. diff --git a/codersdk/users_test.go b/codersdk/users_test.go index 3425c9204f3ca..8e2fcd6881483 100644 --- a/codersdk/users_test.go +++ b/codersdk/users_test.go @@ -10,6 +10,26 @@ import ( "github.com/coder/coder/coderd/coderdtest" ) +func TestHasInitialUser(t *testing.T) { + t.Parallel() + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + has, err := client.HasInitialUser(context.Background()) + require.NoError(t, err) + require.False(t, has) + }) + + t.Run("Found", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + _ = coderdtest.CreateInitialUser(t, client) + has, err := client.HasInitialUser(context.Background()) + require.NoError(t, err) + require.True(t, has) + }) +} + func TestCreateInitialUser(t *testing.T) { t.Parallel() t.Run("Error", func(t *testing.T) { diff --git a/go.mod b/go.mod index d026e00efbe84..b17a5dc50bd16 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,9 @@ replace github.com/hashicorp/terraform-config-inspect => github.com/kylecarbs/te require ( cdr.dev/slog v1.4.1 + github.com/briandowns/spinner v1.18.1 github.com/coder/retry v1.3.0 + github.com/fatih/color v1.13.0 github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/render v1.0.1 github.com/go-playground/validator/v10 v10.10.0 @@ -21,7 +23,10 @@ require ( github.com/hashicorp/terraform-exec v0.15.0 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 github.com/justinas/nosurf v1.1.1 + github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/lib/pq v1.10.4 + github.com/manifoldco/promptui v0.9.0 + github.com/mattn/go-isatty v0.0.14 github.com/moby/moby v20.10.12+incompatible github.com/ory/dockertest/v3 v3.8.1 github.com/pion/datachannel v1.5.2 @@ -51,6 +56,7 @@ require ( github.com/alecthomas/chroma v0.10.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/cenkalti/backoff/v4 v4.1.2 // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/containerd/continuity v0.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dhui/dktest v0.3.9 // indirect @@ -59,7 +65,6 @@ require ( github.com/docker/docker v20.10.12+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/fatih/color v1.13.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -77,7 +82,6 @@ require ( github.com/klauspost/compress v1.13.6 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect diff --git a/go.sum b/go.sum index 0e9fb0192b6ed..a89330f79705e 100644 --- a/go.sum +++ b/go.sum @@ -189,6 +189,8 @@ github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= +github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= @@ -206,8 +208,11 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +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/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= @@ -800,6 +805,8 @@ github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0Lh github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= +github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -854,6 +861,8 @@ github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= @@ -1467,6 +1476,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 24cc781cd023e9b851f7db98e8d78e57cce88788 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 9 Feb 2022 20:47:05 +0000 Subject: [PATCH 2/7] Add CLI test for login --- .vscode/settings.json | 3 ++ cli/clitest/clitest.go | 38 +++++++++++++++++++++++++ cli/config/file_test.go | 2 +- cli/login.go | 15 +++++----- cli/login_test.go | 54 +++++++++++++++++++++++++++++++++++ cli/projectcreate.go | 62 +++++++++++++++++++---------------------- cli/root.go | 19 +++++++++---- go.mod | 2 ++ go.sum | 5 +++- 9 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 cli/clitest/clitest.go create mode 100644 cli/login_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 3178a1698bd5d..1c6d6a8f8c189 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -35,8 +35,11 @@ "goleak", "hashicorp", "httpmw", + "isatty", "Jobf", + "kirsle", "manifoldco", + "mattn", "moby", "nhooyr", "nolint", diff --git a/cli/clitest/clitest.go b/cli/clitest/clitest.go new file mode 100644 index 0000000000000..7166843e93dac --- /dev/null +++ b/cli/clitest/clitest.go @@ -0,0 +1,38 @@ +package clitest + +import ( + "bufio" + "io" + "testing" + + "github.com/spf13/cobra" + + "github.com/coder/coder/cli" + "github.com/coder/coder/cli/config" +) + +func New(t *testing.T, args ...string) (*cobra.Command, config.Root) { + cmd := cli.Root() + dir := t.TempDir() + root := config.Root(dir) + cmd.SetArgs(append([]string{"--global-config", dir}, args...)) + return cmd, root +} + +func StdoutLogs(t *testing.T) io.Writer { + reader, writer := io.Pipe() + scanner := bufio.NewScanner(reader) + t.Cleanup(func() { + _ = reader.Close() + _ = writer.Close() + }) + go func() { + for scanner.Scan() { + if scanner.Err() != nil { + return + } + t.Log(scanner.Text()) + } + }() + return writer +} diff --git a/cli/config/file_test.go b/cli/config/file_test.go index 7e634ac08b420..b3ca15322e217 100644 --- a/cli/config/file_test.go +++ b/cli/config/file_test.go @@ -24,7 +24,7 @@ func TestFile(t *testing.T) { require.NoError(t, err) data, err := root.Session().Read() require.NoError(t, err) - require.Equal(t, "test", string(data)) + require.Equal(t, "test", data) }) t.Run("Delete", func(t *testing.T) { diff --git a/cli/login.go b/cli/login.go index 6edec77d74027..411eb3261f298 100644 --- a/cli/login.go +++ b/cli/login.go @@ -1,18 +1,19 @@ package cli import ( - "errors" "fmt" "net/url" + "os" "strings" - "github.com/coder/coder/coderd" - "github.com/coder/coder/codersdk" "github.com/fatih/color" "github.com/go-playground/validator/v10" "github.com/manifoldco/promptui" "github.com/spf13/cobra" "golang.org/x/xerrors" + + "github.com/coder/coder/coderd" + "github.com/coder/coder/codersdk" ) func login() *cobra.Command { @@ -46,7 +47,7 @@ func login() *cobra.Command { if !isTTY(cmd.InOrStdin()) { return xerrors.New("the initial user cannot be created in non-interactive mode. use the API") } - fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been setup!\n", color.HiBlackString(">")) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been setup!\n", color.HiBlackString(">")) _, err := runPrompt(cmd, &promptui.Prompt{ Label: "Would you like to create the first user?", @@ -59,7 +60,7 @@ func login() *cobra.Command { username, err := runPrompt(cmd, &promptui.Prompt{ Label: "What username would you like?", - Default: "kyle", + Default: os.Getenv("USER"), }) if err != nil { return err @@ -78,7 +79,7 @@ func login() *cobra.Command { Validate: func(s string) error { err := validator.New().Var(s, "email") if err != nil { - return errors.New("That's not a valid email address!") + return xerrors.New("That's not a valid email address!") } return err }, @@ -121,7 +122,7 @@ func login() *cobra.Command { return xerrors.Errorf("write server url: %w", err) } - fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're logged in.\n", color.HiBlackString(">"), color.HiCyanString(username)) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're logged in.\n", color.HiBlackString(">"), color.HiCyanString(username)) return nil } diff --git a/cli/login_test.go b/cli/login_test.go new file mode 100644 index 0000000000000..160d539bac8ea --- /dev/null +++ b/cli/login_test.go @@ -0,0 +1,54 @@ +package cli_test + +import ( + "testing" + + "github.com/coder/coder/cli/clitest" + "github.com/coder/coder/coderd/coderdtest" + "github.com/stretchr/testify/require" + + "github.com/Netflix/go-expect" +) + +func TestLogin(t *testing.T) { + t.Parallel() + t.Run("InitialUserNoTTY", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t) + root, _ := clitest.New(t, "login", client.URL.String()) + err := root.Execute() + require.Error(t, err) + }) + + t.Run("InitialUserTTY", func(t *testing.T) { + t.Parallel() + console, err := expect.NewConsole(expect.WithStdout(clitest.StdoutLogs(t))) + require.NoError(t, err) + client := coderdtest.New(t) + root, _ := clitest.New(t, "login", client.URL.String()) + root.SetIn(console.Tty()) + root.SetOut(console.Tty()) + go func() { + err = root.Execute() + require.NoError(t, err) + }() + + matches := []string{ + "first user?", "y", + "username", "testuser", + "organization", "testorg", + "email", "user@coder.com", + "password", "password", + } + for i := 0; i < len(matches); i += 2 { + match := matches[i] + value := matches[i+1] + _, err = console.ExpectString(match) + require.NoError(t, err) + _, err = console.SendLine(value) + require.NoError(t, err) + } + _, err = console.ExpectString("Welcome to Coder") + require.NoError(t, err) + }) +} diff --git a/cli/projectcreate.go b/cli/projectcreate.go index 5d8d000971456..eb5219cb816cb 100644 --- a/cli/projectcreate.go +++ b/cli/projectcreate.go @@ -11,13 +11,14 @@ import ( "time" "github.com/briandowns/spinner" - "github.com/coder/coder/coderd" - "github.com/coder/coder/codersdk" - "github.com/coder/coder/database" "github.com/fatih/color" "github.com/manifoldco/promptui" "github.com/spf13/cobra" "golang.org/x/xerrors" + + "github.com/coder/coder/coderd" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/database" ) func projectCreate() *cobra.Command { @@ -48,9 +49,8 @@ func projectCreate() *cobra.Command { return err } - name := filepath.Base(workingDir) - name, err = runPrompt(cmd, &promptui.Prompt{ - Default: name, + name, err := runPrompt(cmd, &promptui.Prompt{ + Default: filepath.Base(workingDir), Label: "What's your project's name?", Validate: func(s string) error { _, err = client.Project(cmd.Context(), organization.Name, s) @@ -69,7 +69,7 @@ func projectCreate() *cobra.Command { spin.Start() defer spin.Stop() - bytes, err := tarDir(workingDir) + bytes, err := tarDirectory(workingDir) if err != nil { return err } @@ -91,8 +91,6 @@ func projectCreate() *cobra.Command { } spin.Stop() - time.Sleep(time.Second) - logs, err := client.FollowProvisionerJobLogsAfter(context.Background(), organization.Name, job.ID, time.Time{}) if err != nil { return err @@ -102,54 +100,50 @@ func projectCreate() *cobra.Command { if !ok { break } - fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[parse]"), log.Output) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[parse]"), log.Output) } - fmt.Printf("Projects %+v %+v\n", projects, organization) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Create project %q!\n", name) return nil }, } } -func tarDir(directory string) ([]byte, error) { +func tarDirectory(directory string) ([]byte, error) { var buffer bytes.Buffer - tw := tar.NewWriter(&buffer) - // walk through every file in the folder - err := filepath.Walk(directory, func(file string, fi os.FileInfo, err error) error { - // generate tar header - header, err := tar.FileInfoHeader(fi, file) + tarWriter := tar.NewWriter(&buffer) + err := filepath.Walk(directory, func(file string, fileInfo os.FileInfo, err error) error { + if err != nil { + return err + } + header, err := tar.FileInfoHeader(fileInfo, file) if err != nil { return err } - - // must provide real name - // (see https://golang.org/src/archive/tar/common.go?#L626) rel, err := filepath.Rel(directory, file) if err != nil { return err } header.Name = rel - - // write header - if err := tw.WriteHeader(header); err != nil { + if err := tarWriter.WriteHeader(header); err != nil { return err } - // if not a dir, write file content - if !fi.IsDir() { - data, err := os.Open(file) - if err != nil { - return err - } - if _, err := io.Copy(tw, data); err != nil { - return err - } + if fileInfo.IsDir() { + return nil + } + data, err := os.Open(file) + if err != nil { + return err + } + if _, err := io.Copy(tarWriter, data); err != nil { + return err } - return nil + return data.Close() }) if err != nil { return nil, err } - err = tw.Flush() + err = tarWriter.Flush() if err != nil { return nil, err } diff --git a/cli/root.go b/cli/root.go index 95a27116db80d..85db65385291a 100644 --- a/cli/root.go +++ b/cli/root.go @@ -7,14 +7,16 @@ import ( "os" "strings" - "github.com/coder/coder/cli/config" - "github.com/coder/coder/coderd" - "github.com/coder/coder/codersdk" "github.com/fatih/color" "github.com/kirsle/configdir" "github.com/manifoldco/promptui" "github.com/mattn/go-isatty" "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/coder/coder/cli/config" + "github.com/coder/coder/coderd" + "github.com/coder/coder/codersdk" ) const ( @@ -115,8 +117,15 @@ func isTTY(reader io.Reader) bool { } func runPrompt(cmd *cobra.Command, prompt *promptui.Prompt) (string, error) { - prompt.Stdin = cmd.InOrStdin().(io.ReadCloser) - prompt.Stdout = cmd.OutOrStdout().(io.WriteCloser) + var ok bool + prompt.Stdin, ok = cmd.InOrStdin().(io.ReadCloser) + if !ok { + return "", xerrors.New("stdin must be a readcloser") + } + prompt.Stdout, ok = cmd.OutOrStdout().(io.WriteCloser) + if !ok { + return "", xerrors.New("stdout must be a readcloser") + } // The prompt library displays defaults in a jarring way for the user // by attempting to autocomplete it. This sets no default enabling us diff --git a/go.mod b/go.mod index b17a5dc50bd16..785dd6b8ba330 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ replace github.com/hashicorp/terraform-config-inspect => github.com/kylecarbs/te require ( cdr.dev/slog v1.4.1 + github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/briandowns/spinner v1.18.1 github.com/coder/retry v1.3.0 github.com/fatih/color v1.13.0 @@ -58,6 +59,7 @@ require ( github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/containerd/continuity v0.2.2 // indirect + github.com/creack/pty v1.1.17 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dhui/dktest v0.3.9 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect diff --git a/go.sum b/go.sum index a89330f79705e..e82f6e25638cb 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,8 @@ github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01 github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -350,8 +352,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= From ddb57566665ca1aa778da875f5b0107eb71310d8 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 9 Feb 2022 20:52:17 +0000 Subject: [PATCH 3/7] Add "bin/coder" target to Makefile --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7afdc25f4f206..82e02228f6688 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,14 @@ +bin/coder: + mkdir -p bin + go build -o bin/coder cmd/coder/main.go +.PHONY: bin/coder + bin/coderd: mkdir -p bin go build -o bin/coderd cmd/coderd/main.go .PHONY: bin/coderd -build: site/out bin/coderd +build: site/out bin/coder bin/coderd .PHONY: build # Runs migrations to output a dump of the database. From d8b4e88a535f630678f6384801b3a8ad2147a925 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 9 Feb 2022 22:04:12 +0000 Subject: [PATCH 4/7] Update promptui to fix race --- go.mod | 5 +++++ go.sum | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 785dd6b8ba330..1627034bf6e9f 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/coder/coder go 1.17 +// Required until https://github.com/manifoldco/promptui/pull/169 is merged. +replace github.com/manifoldco/promptui => github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2 + // Required until https://github.com/hashicorp/terraform-exec/pull/275 and https://github.com/hashicorp/terraform-exec/pull/276 are merged. replace github.com/hashicorp/terraform-exec => github.com/kylecarbs/terraform-exec v0.15.1-0.20220202050609-a1ce7181b180 @@ -81,8 +84,10 @@ require ( github.com/hashicorp/terraform-json v0.13.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/leodido/go-urn v1.2.1 // indirect + github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect diff --git a/go.sum b/go.sum index e82f6e25638cb..40bc40a395fec 100644 --- a/go.sum +++ b/go.sum @@ -795,6 +795,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= @@ -839,6 +841,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= +github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2 h1:MUREBTh4kybLY1KyuBfSx+QPfTB8XiUHs6ZxUhOPTnU= +github.com/kylecarbs/promptui v0.8.1-0.20201231190244-d8f2159af2b2/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= github.com/kylecarbs/terraform-config-inspect v0.0.0-20211215004401-bbc517866b88 h1:tvG/qs5c4worwGyGnbbb4i/dYYLjpFwDMqcIT3awAf8= github.com/kylecarbs/terraform-config-inspect v0.0.0-20211215004401-bbc517866b88/go.mod h1:Z0Nnk4+3Cy89smEbrq+sl1bxc9198gIP4I7wcQF6Kqs= github.com/kylecarbs/terraform-exec v0.15.1-0.20220202050609-a1ce7181b180 h1:yafC0pmxjs18fnO5RdKFLSItJIjYwGfSHTfcUvlZb3E= @@ -858,14 +862,14 @@ github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= -github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= From fc6d9da4c2c371f3ab9d7f8649a578d3ea3df6d2 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 9 Feb 2022 22:07:47 +0000 Subject: [PATCH 5/7] Fix error scope --- cli/login_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/login_test.go b/cli/login_test.go index 160d539bac8ea..1e695a4fcba39 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -29,7 +29,7 @@ func TestLogin(t *testing.T) { root.SetIn(console.Tty()) root.SetOut(console.Tty()) go func() { - err = root.Execute() + err := root.Execute() require.NoError(t, err) }() From 4f74b01c94c3f20a98846372a0d90bf18f4e6f2d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 10 Feb 2022 14:15:48 +0000 Subject: [PATCH 6/7] Don't run CLI tests on Windows --- cli/login_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/login_test.go b/cli/login_test.go index 1e695a4fcba39..f2102177d6710 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -1,3 +1,5 @@ +//go:build !windows + package cli_test import ( From aed4a621f90b3673638e3cf0517e8e9c5210e8c8 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 10 Feb 2022 14:25:50 +0000 Subject: [PATCH 7/7] Fix requested changes --- cli/login.go | 27 +++++++++++++++------------ coderd/cmd/root.go | 10 ++++------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/cli/login.go b/cli/login.go index 411eb3261f298..73758719d0128 100644 --- a/cli/login.go +++ b/cli/login.go @@ -3,7 +3,7 @@ package cli import ( "fmt" "net/url" - "os" + "os/user" "strings" "github.com/fatih/color" @@ -31,7 +31,7 @@ func login() *cobra.Command { } serverURL, err := url.Parse(rawURL) if err != nil { - return err + return xerrors.Errorf("parse raw url %q: %w", rawURL, err) } // Default to HTTPs. Enables simple URLs like: master.cdr.dev if serverURL.Scheme == "" { @@ -41,13 +41,13 @@ func login() *cobra.Command { client := codersdk.New(serverURL) hasInitialUser, err := client.HasInitialUser(cmd.Context()) if err != nil { - return err + return xerrors.Errorf("has initial user: %w", err) } if !hasInitialUser { if !isTTY(cmd.InOrStdin()) { return xerrors.New("the initial user cannot be created in non-interactive mode. use the API") } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been setup!\n", color.HiBlackString(">")) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been set up!\n", color.HiBlackString(">")) _, err := runPrompt(cmd, &promptui.Prompt{ Label: "Would you like to create the first user?", @@ -55,15 +55,18 @@ func login() *cobra.Command { Default: "y", }) if err != nil { - return err + return xerrors.Errorf("create user prompt: %w", err) + } + currentUser, err := user.Current() + if err != nil { + return xerrors.Errorf("get current user: %w", err) } - username, err := runPrompt(cmd, &promptui.Prompt{ Label: "What username would you like?", - Default: os.Getenv("USER"), + Default: currentUser.Username, }) if err != nil { - return err + return xerrors.Errorf("pick username prompt: %w", err) } organization, err := runPrompt(cmd, &promptui.Prompt{ @@ -71,7 +74,7 @@ func login() *cobra.Command { Default: "acme-corp", }) if err != nil { - return err + return xerrors.Errorf("pick organization prompt: %w", err) } email, err := runPrompt(cmd, &promptui.Prompt{ @@ -85,7 +88,7 @@ func login() *cobra.Command { }, }) if err != nil { - return err + return xerrors.Errorf("specify email prompt: %w", err) } password, err := runPrompt(cmd, &promptui.Prompt{ @@ -93,7 +96,7 @@ func login() *cobra.Command { Mask: '*', }) if err != nil { - return err + return xerrors.Errorf("specify password prompt: %w", err) } _, err = client.CreateInitialUser(cmd.Context(), coderd.CreateInitialUserRequest{ @@ -122,7 +125,7 @@ func login() *cobra.Command { return xerrors.Errorf("write server url: %w", err) } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're logged in.\n", color.HiBlackString(">"), color.HiCyanString(username)) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(username)) return nil } diff --git a/coderd/cmd/root.go b/coderd/cmd/root.go index 4c3f0d602816d..9087ac435b715 100644 --- a/coderd/cmd/root.go +++ b/coderd/cmd/root.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "fmt" "io" "io/ioutil" "net" @@ -46,11 +45,10 @@ func Root() *cobra.Command { } defer listener.Close() - serverURL, err := url.Parse(fmt.Sprintf("http://%s", address)) - if err != nil { - return xerrors.Errorf("parse %q: %w", address, err) - } - client := codersdk.New(serverURL) + client := codersdk.New(&url.URL{ + Scheme: "http", + Host: address, + }) closer, err := newProvisionerDaemon(cmd.Context(), client, logger) if err != nil { return xerrors.Errorf("create provisioner daemon: %w", err) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy