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"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/coder/serpent"
|
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||||
"github.com/coder/coder/v2/coderd/notifications"
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/dispatch"
|
"github.com/coder/coder/v2/coderd/notifications/dispatch"
|
||||||
"github.com/coder/coder/v2/coderd/notifications/types"
|
"github.com/coder/coder/v2/coderd/notifications/types"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
"github.com/coder/serpent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBufferedUpdates(t *testing.T) {
|
func TestBufferedUpdates(t *testing.T) {
|
||||||
|
@ -267,10 +267,10 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProvisionerKey struct {
|
type ProvisionerKey struct {
|
||||||
ID uuid.UUID `json:"id" format:"uuid"`
|
ID uuid.UUID `json:"id" table:"-" format:"uuid"`
|
||||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
CreatedAt time.Time `json:"created_at" table:"created_at" format:"date-time"`
|
||||||
OrganizationID uuid.UUID `json:"organization" format:"uuid"`
|
OrganizationID uuid.UUID `json:"organization" table:"organization_id" format:"uuid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" table:"name,default_sort"`
|
||||||
// HashedSecret - never include the access token in the API response
|
// 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
|
Manage provisioner daemons
|
||||||
|
|
||||||
|
Aliases:
|
||||||
|
|
||||||
|
- provisioner
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
@ -39,8 +39,10 @@ func (r *RootCmd) provisionerDaemons() *serpent.Command {
|
|||||||
Handler: func(inv *serpent.Invocation) error {
|
Handler: func(inv *serpent.Invocation) error {
|
||||||
return inv.Command.HelpHandler(inv)
|
return inv.Command.HelpHandler(inv)
|
||||||
},
|
},
|
||||||
|
Aliases: []string{"provisioner"},
|
||||||
Children: []*serpent.Command{
|
Children: []*serpent.Command{
|
||||||
r.provisionerDaemonStart(),
|
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
|
Manage provisioner daemons
|
||||||
|
|
||||||
|
Aliases: provisioner
|
||||||
|
|
||||||
SUBCOMMANDS:
|
SUBCOMMANDS:
|
||||||
start Run a provisioner daemon
|
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