feat: add provisioner key cli commands (#13875)

This commit is contained in:
Garrett Delfosse
2024-07-18 14:44:20 -04:00
committed by GitHub
parent 91cbe679c0
commit f975701b34
11 changed files with 361 additions and 6 deletions

View File

@ -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) {

View File

@ -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
}

View File

@ -4,6 +4,10 @@
Manage provisioner daemons
Aliases:
- provisioner
## Usage
```console

View File

@ -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(),
},
}

View File

@ -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 <name>",
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 <name>",
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
}

View File

@ -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")
})
}

View File

@ -5,6 +5,8 @@ USAGE:
Manage provisioner daemons
Aliases: provisioner
SUBCOMMANDS:
start Run a provisioner daemon

View File

@ -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.

View File

@ -0,0 +1,13 @@
coder v0.0.0-devel
USAGE:
coder provisionerd keys create [flags] <name>
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.

View File

@ -0,0 +1,18 @@
coder v0.0.0-devel
USAGE:
coder provisionerd keys delete [flags] <name>
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.

View File

@ -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.