diff --git a/coderd/notifications/manager_test.go b/coderd/notifications/manager_test.go index 93ba158b48a65..2e264c534ccfa 100644 --- a/coderd/notifications/manager_test.go +++ b/coderd/notifications/manager_test.go @@ -12,14 +12,13 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/xerrors" - "github.com/coder/serpent" - "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/notifications/dispatch" "github.com/coder/coder/v2/coderd/notifications/types" "github.com/coder/coder/v2/testutil" + "github.com/coder/serpent" ) func TestBufferedUpdates(t *testing.T) { diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 605d44d88c071..d6a8ba1e6f2fe 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -267,10 +267,10 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione } type ProvisionerKey struct { - ID uuid.UUID `json:"id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - OrganizationID uuid.UUID `json:"organization" format:"uuid"` - Name string `json:"name"` + ID uuid.UUID `json:"id" table:"-" format:"uuid"` + CreatedAt time.Time `json:"created_at" table:"created_at" format:"date-time"` + OrganizationID uuid.UUID `json:"organization" table:"organization_id" format:"uuid"` + Name string `json:"name" table:"name,default_sort"` // HashedSecret - never include the access token in the API response } diff --git a/docs/cli/provisionerd.md b/docs/cli/provisionerd.md index 21af8ff547fcb..44168c53a602d 100644 --- a/docs/cli/provisionerd.md +++ b/docs/cli/provisionerd.md @@ -4,6 +4,10 @@ Manage provisioner daemons +Aliases: + +- provisioner + ## Usage ```console diff --git a/enterprise/cli/provisionerdaemons.go b/enterprise/cli/provisionerdaemons.go index 079b1891346eb..2ea8983adc69d 100644 --- a/enterprise/cli/provisionerdaemons.go +++ b/enterprise/cli/provisionerdaemons.go @@ -39,8 +39,10 @@ func (r *RootCmd) provisionerDaemons() *serpent.Command { Handler: func(inv *serpent.Invocation) error { return inv.Command.HelpHandler(inv) }, + Aliases: []string{"provisioner"}, Children: []*serpent.Command{ r.provisionerDaemonStart(), + r.provisionerKeys(), }, } diff --git a/enterprise/cli/provisionerkeys.go b/enterprise/cli/provisionerkeys.go new file mode 100644 index 0000000000000..8253d4826e164 --- /dev/null +++ b/enterprise/cli/provisionerkeys.go @@ -0,0 +1,175 @@ +package cli + +import ( + "fmt" + "strings" + + "golang.org/x/xerrors" + + agpl "github.com/coder/coder/v2/cli" + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/pretty" + "github.com/coder/serpent" +) + +func (r *RootCmd) provisionerKeys() *serpent.Command { + cmd := &serpent.Command{ + Use: "keys", + Short: "Manage provisioner keys", + Handler: func(inv *serpent.Invocation) error { + return inv.Command.HelpHandler(inv) + }, + Hidden: true, + Aliases: []string{"key"}, + Children: []*serpent.Command{ + r.provisionerKeysCreate(), + r.provisionerKeysList(), + r.provisionerKeysDelete(), + }, + } + + return cmd +} + +func (r *RootCmd) provisionerKeysCreate() *serpent.Command { + orgContext := agpl.NewOrganizationContext() + + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "create ", + Short: "Create a new provisioner key", + Middleware: serpent.Chain( + serpent.RequireNArgs(1), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + ctx := inv.Context() + + org, err := orgContext.Selected(inv, client) + if err != nil { + return xerrors.Errorf("current organization: %w", err) + } + + res, err := client.CreateProvisionerKey(ctx, org.ID, codersdk.CreateProvisionerKeyRequest{ + Name: inv.Args[0], + }) + if err != nil { + return xerrors.Errorf("create provisioner key: %w", err) + } + + _, _ = fmt.Fprintf( + inv.Stdout, + "Successfully created provisioner key %s! Save this authentication token, it will not be shown again.\n\n%s\n", + pretty.Sprint(cliui.DefaultStyles.Keyword, strings.ToLower(inv.Args[0])), + pretty.Sprint(cliui.DefaultStyles.Keyword, res.Key), + ) + + return nil + }, + } + + cmd.Options = serpent.OptionSet{} + orgContext.AttachOptions(cmd) + + return cmd +} + +func (r *RootCmd) provisionerKeysList() *serpent.Command { + var ( + orgContext = agpl.NewOrganizationContext() + formatter = cliui.NewOutputFormatter( + cliui.TableFormat([]codersdk.ProvisionerKey{}, nil), + cliui.JSONFormat(), + ) + ) + + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "list", + Short: "List provisioner keys in an organization", + Aliases: []string{"ls"}, + Middleware: serpent.Chain( + serpent.RequireNArgs(0), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + ctx := inv.Context() + + org, err := orgContext.Selected(inv, client) + if err != nil { + return xerrors.Errorf("current organization: %w", err) + } + + keys, err := client.ListProvisionerKeys(ctx, org.ID) + if err != nil { + return xerrors.Errorf("list provisioner keys: %w", err) + } + + if len(keys) == 0 { + _, _ = fmt.Fprintln(inv.Stdout, "No provisioner keys found") + return nil + } + + out, err := formatter.Format(inv.Context(), keys) + if err != nil { + return xerrors.Errorf("display provisioner keys: %w", err) + } + + _, _ = fmt.Fprintln(inv.Stdout, out) + + return nil + }, + } + + cmd.Options = serpent.OptionSet{} + orgContext.AttachOptions(cmd) + + return cmd +} + +func (r *RootCmd) provisionerKeysDelete() *serpent.Command { + orgContext := agpl.NewOrganizationContext() + + client := new(codersdk.Client) + cmd := &serpent.Command{ + Use: "delete ", + Short: "Delete a provisioner key", + Middleware: serpent.Chain( + serpent.RequireNArgs(1), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + ctx := inv.Context() + + org, err := orgContext.Selected(inv, client) + if err != nil { + return xerrors.Errorf("current organization: %w", err) + } + + _, err = cliui.Prompt(inv, cliui.PromptOptions{ + Text: fmt.Sprintf("Are you sure you want to delete provisioner key %s?", pretty.Sprint(cliui.DefaultStyles.Keyword, inv.Args[0])), + IsConfirm: true, + }) + if err != nil { + return err + } + + err = client.DeleteProvisionerKey(ctx, org.ID, inv.Args[0]) + if err != nil { + return xerrors.Errorf("delete provisioner key: %w", err) + } + + _, _ = fmt.Fprintf(inv.Stdout, "Successfully deleted provisioner key %s!\n", pretty.Sprint(cliui.DefaultStyles.Keyword, strings.ToLower(inv.Args[0]))) + + return nil + }, + } + + cmd.Options = serpent.OptionSet{ + cliui.SkipPromptOption(), + } + orgContext.AttachOptions(cmd) + + return cmd +} diff --git a/enterprise/cli/provisionerkeys_test.go b/enterprise/cli/provisionerkeys_test.go new file mode 100644 index 0000000000000..dac764da616b9 --- /dev/null +++ b/enterprise/cli/provisionerkeys_test.go @@ -0,0 +1,111 @@ +package cli_test + +import ( + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/coder/v2/pty/ptytest" + "github.com/coder/coder/v2/testutil" +) + +func TestProvisionerKeys(t *testing.T) { + t.Parallel() + + t.Run("CRUD", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)} + client, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + DeploymentValues: dv, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureMultipleOrganizations: 1, + }, + }, + }) + orgAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID)) + + name := "dont-TEST-me" + ctx := testutil.Context(t, testutil.WaitMedium) + inv, conf := newCLI( + t, + "provisioner", "keys", "create", name, + ) + + pty := ptytest.New(t) + inv.Stdout = pty.Output() + clitest.SetupConfig(t, orgAdminClient, conf) + + err := inv.WithContext(ctx).Run() + require.NoError(t, err) + + line := pty.ReadLine(ctx) + require.Contains(t, line, "Successfully created provisioner key") + require.Contains(t, line, strings.ToLower(name)) + // empty line + _ = pty.ReadLine(ctx) + key := pty.ReadLine(ctx) + require.NotEmpty(t, key) + parts := strings.Split(key, ":") + require.Len(t, parts, 2, "expected 2 parts") + _, err = uuid.Parse(parts[0]) + require.NoError(t, err, "expected token to be a uuid") + + inv, conf = newCLI( + t, + "provisioner", "keys", "ls", + ) + pty = ptytest.New(t) + inv.Stdout = pty.Output() + clitest.SetupConfig(t, orgAdminClient, conf) + + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + line = pty.ReadLine(ctx) + require.Contains(t, line, "NAME") + require.Contains(t, line, "CREATED AT") + require.Contains(t, line, "ORGANIZATION ID") + line = pty.ReadLine(ctx) + require.Contains(t, line, strings.ToLower(name)) + + inv, conf = newCLI( + t, + "provisioner", "keys", "delete", "-y", name, + ) + + pty = ptytest.New(t) + inv.Stdout = pty.Output() + clitest.SetupConfig(t, orgAdminClient, conf) + + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + line = pty.ReadLine(ctx) + require.Contains(t, line, "Successfully deleted provisioner key") + require.Contains(t, line, strings.ToLower(name)) + + inv, conf = newCLI( + t, + "provisioner", "keys", "ls", + ) + pty = ptytest.New(t) + inv.Stdout = pty.Output() + clitest.SetupConfig(t, orgAdminClient, conf) + + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + line = pty.ReadLine(ctx) + require.Contains(t, line, "No provisioner keys found") + }) +} diff --git a/enterprise/cli/testdata/coder_provisionerd_--help.golden b/enterprise/cli/testdata/coder_provisionerd_--help.golden index bfa9ec147e03d..175c33e02f973 100644 --- a/enterprise/cli/testdata/coder_provisionerd_--help.golden +++ b/enterprise/cli/testdata/coder_provisionerd_--help.golden @@ -5,6 +5,8 @@ USAGE: Manage provisioner daemons + Aliases: provisioner + SUBCOMMANDS: start Run a provisioner daemon diff --git a/enterprise/cli/testdata/coder_provisionerd_keys_--help.golden b/enterprise/cli/testdata/coder_provisionerd_keys_--help.golden new file mode 100644 index 0000000000000..68b7b5223a3e0 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisionerd_keys_--help.golden @@ -0,0 +1,16 @@ +coder v0.0.0-devel + +USAGE: + coder provisionerd keys + + Manage provisioner keys + + Aliases: key + +SUBCOMMANDS: + create Create a new provisioner key + delete Delete a provisioner key + list List provisioner keys + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisionerd_keys_create_--help.golden b/enterprise/cli/testdata/coder_provisionerd_keys_create_--help.golden new file mode 100644 index 0000000000000..a1e7cd1aa9404 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisionerd_keys_create_--help.golden @@ -0,0 +1,13 @@ +coder v0.0.0-devel + +USAGE: + coder provisionerd keys create [flags] + + Create a new provisioner key + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisionerd_keys_delete_--help.golden b/enterprise/cli/testdata/coder_provisionerd_keys_delete_--help.golden new file mode 100644 index 0000000000000..0ab277f6609e7 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisionerd_keys_delete_--help.golden @@ -0,0 +1,18 @@ +coder v0.0.0-devel + +USAGE: + coder provisionerd keys delete [flags] + + Delete a provisioner key + + Aliases: rm + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + + -y, --yes bool + Bypass prompts. + +——— +Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_provisionerd_keys_list_--help.golden b/enterprise/cli/testdata/coder_provisionerd_keys_list_--help.golden new file mode 100644 index 0000000000000..0bdb43afff4e8 --- /dev/null +++ b/enterprise/cli/testdata/coder_provisionerd_keys_list_--help.golden @@ -0,0 +1,15 @@ +coder v0.0.0-devel + +USAGE: + coder provisionerd keys list [flags] + + List provisioner keys + + Aliases: ls + +OPTIONS: + -O, --org string, $CODER_ORGANIZATION + Select which organization (uuid or name) to use. + +——— +Run `coder --help` for a list of global options.