mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
feat: add provisioner key cli commands (#13875)
This commit is contained in:
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
4
docs/cli/provisionerd.md
generated
4
docs/cli/provisionerd.md
generated
@ -4,6 +4,10 @@
|
||||
|
||||
Manage provisioner daemons
|
||||
|
||||
Aliases:
|
||||
|
||||
- provisioner
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
|
@ -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(),
|
||||
},
|
||||
}
|
||||
|
||||
|
175
enterprise/cli/provisionerkeys.go
Normal file
175
enterprise/cli/provisionerkeys.go
Normal 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
|
||||
}
|
111
enterprise/cli/provisionerkeys_test.go
Normal file
111
enterprise/cli/provisionerkeys_test.go
Normal 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")
|
||||
})
|
||||
}
|
@ -5,6 +5,8 @@ USAGE:
|
||||
|
||||
Manage provisioner daemons
|
||||
|
||||
Aliases: provisioner
|
||||
|
||||
SUBCOMMANDS:
|
||||
start Run a provisioner daemon
|
||||
|
||||
|
16
enterprise/cli/testdata/coder_provisionerd_keys_--help.golden
vendored
Normal file
16
enterprise/cli/testdata/coder_provisionerd_keys_--help.golden
vendored
Normal 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.
|
13
enterprise/cli/testdata/coder_provisionerd_keys_create_--help.golden
vendored
Normal file
13
enterprise/cli/testdata/coder_provisionerd_keys_create_--help.golden
vendored
Normal 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.
|
18
enterprise/cli/testdata/coder_provisionerd_keys_delete_--help.golden
vendored
Normal file
18
enterprise/cli/testdata/coder_provisionerd_keys_delete_--help.golden
vendored
Normal 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.
|
15
enterprise/cli/testdata/coder_provisionerd_keys_list_--help.golden
vendored
Normal file
15
enterprise/cli/testdata/coder_provisionerd_keys_list_--help.golden
vendored
Normal 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.
|
Reference in New Issue
Block a user