Skip to content

Commit f975701

Browse files
authored
feat: add provisioner key cli commands (coder#13875)
1 parent 91cbe67 commit f975701

11 files changed

+361
-6
lines changed

coderd/notifications/manager_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ import (
1212
"github.com/stretchr/testify/require"
1313
"golang.org/x/xerrors"
1414

15-
"github.com/coder/serpent"
16-
1715
"github.com/coder/coder/v2/coderd/database"
1816
"github.com/coder/coder/v2/coderd/database/dbgen"
1917
"github.com/coder/coder/v2/coderd/notifications"
2018
"github.com/coder/coder/v2/coderd/notifications/dispatch"
2119
"github.com/coder/coder/v2/coderd/notifications/types"
2220
"github.com/coder/coder/v2/testutil"
21+
"github.com/coder/serpent"
2322
)
2423

2524
func TestBufferedUpdates(t *testing.T) {

codersdk/provisionerdaemons.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,10 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
267267
}
268268

269269
type ProvisionerKey struct {
270-
ID uuid.UUID `json:"id" format:"uuid"`
271-
CreatedAt time.Time `json:"created_at" format:"date-time"`
272-
OrganizationID uuid.UUID `json:"organization" format:"uuid"`
273-
Name string `json:"name"`
270+
ID uuid.UUID `json:"id" table:"-" format:"uuid"`
271+
CreatedAt time.Time `json:"created_at" table:"created_at" format:"date-time"`
272+
OrganizationID uuid.UUID `json:"organization" table:"organization_id" format:"uuid"`
273+
Name string `json:"name" table:"name,default_sort"`
274274
// HashedSecret - never include the access token in the API response
275275
}
276276

docs/cli/provisionerd.md

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/cli/provisionerdaemons.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ func (r *RootCmd) provisionerDaemons() *serpent.Command {
3939
Handler: func(inv *serpent.Invocation) error {
4040
return inv.Command.HelpHandler(inv)
4141
},
42+
Aliases: []string{"provisioner"},
4243
Children: []*serpent.Command{
4344
r.provisionerDaemonStart(),
45+
r.provisionerKeys(),
4446
},
4547
}
4648

enterprise/cli/provisionerkeys.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"golang.org/x/xerrors"
8+
9+
agpl "github.com/coder/coder/v2/cli"
10+
"github.com/coder/coder/v2/cli/cliui"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/pretty"
13+
"github.com/coder/serpent"
14+
)
15+
16+
func (r *RootCmd) provisionerKeys() *serpent.Command {
17+
cmd := &serpent.Command{
18+
Use: "keys",
19+
Short: "Manage provisioner keys",
20+
Handler: func(inv *serpent.Invocation) error {
21+
return inv.Command.HelpHandler(inv)
22+
},
23+
Hidden: true,
24+
Aliases: []string{"key"},
25+
Children: []*serpent.Command{
26+
r.provisionerKeysCreate(),
27+
r.provisionerKeysList(),
28+
r.provisionerKeysDelete(),
29+
},
30+
}
31+
32+
return cmd
33+
}
34+
35+
func (r *RootCmd) provisionerKeysCreate() *serpent.Command {
36+
orgContext := agpl.NewOrganizationContext()
37+
38+
client := new(codersdk.Client)
39+
cmd := &serpent.Command{
40+
Use: "create <name>",
41+
Short: "Create a new provisioner key",
42+
Middleware: serpent.Chain(
43+
serpent.RequireNArgs(1),
44+
r.InitClient(client),
45+
),
46+
Handler: func(inv *serpent.Invocation) error {
47+
ctx := inv.Context()
48+
49+
org, err := orgContext.Selected(inv, client)
50+
if err != nil {
51+
return xerrors.Errorf("current organization: %w", err)
52+
}
53+
54+
res, err := client.CreateProvisionerKey(ctx, org.ID, codersdk.CreateProvisionerKeyRequest{
55+
Name: inv.Args[0],
56+
})
57+
if err != nil {
58+
return xerrors.Errorf("create provisioner key: %w", err)
59+
}
60+
61+
_, _ = fmt.Fprintf(
62+
inv.Stdout,
63+
"Successfully created provisioner key %s! Save this authentication token, it will not be shown again.\n\n%s\n",
64+
pretty.Sprint(cliui.DefaultStyles.Keyword, strings.ToLower(inv.Args[0])),
65+
pretty.Sprint(cliui.DefaultStyles.Keyword, res.Key),
66+
)
67+
68+
return nil
69+
},
70+
}
71+
72+
cmd.Options = serpent.OptionSet{}
73+
orgContext.AttachOptions(cmd)
74+
75+
return cmd
76+
}
77+
78+
func (r *RootCmd) provisionerKeysList() *serpent.Command {
79+
var (
80+
orgContext = agpl.NewOrganizationContext()
81+
formatter = cliui.NewOutputFormatter(
82+
cliui.TableFormat([]codersdk.ProvisionerKey{}, nil),
83+
cliui.JSONFormat(),
84+
)
85+
)
86+
87+
client := new(codersdk.Client)
88+
cmd := &serpent.Command{
89+
Use: "list",
90+
Short: "List provisioner keys in an organization",
91+
Aliases: []string{"ls"},
92+
Middleware: serpent.Chain(
93+
serpent.RequireNArgs(0),
94+
r.InitClient(client),
95+
),
96+
Handler: func(inv *serpent.Invocation) error {
97+
ctx := inv.Context()
98+
99+
org, err := orgContext.Selected(inv, client)
100+
if err != nil {
101+
return xerrors.Errorf("current organization: %w", err)
102+
}
103+
104+
keys, err := client.ListProvisionerKeys(ctx, org.ID)
105+
if err != nil {
106+
return xerrors.Errorf("list provisioner keys: %w", err)
107+
}
108+
109+
if len(keys) == 0 {
110+
_, _ = fmt.Fprintln(inv.Stdout, "No provisioner keys found")
111+
return nil
112+
}
113+
114+
out, err := formatter.Format(inv.Context(), keys)
115+
if err != nil {
116+
return xerrors.Errorf("display provisioner keys: %w", err)
117+
}
118+
119+
_, _ = fmt.Fprintln(inv.Stdout, out)
120+
121+
return nil
122+
},
123+
}
124+
125+
cmd.Options = serpent.OptionSet{}
126+
orgContext.AttachOptions(cmd)
127+
128+
return cmd
129+
}
130+
131+
func (r *RootCmd) provisionerKeysDelete() *serpent.Command {
132+
orgContext := agpl.NewOrganizationContext()
133+
134+
client := new(codersdk.Client)
135+
cmd := &serpent.Command{
136+
Use: "delete <name>",
137+
Short: "Delete a provisioner key",
138+
Middleware: serpent.Chain(
139+
serpent.RequireNArgs(1),
140+
r.InitClient(client),
141+
),
142+
Handler: func(inv *serpent.Invocation) error {
143+
ctx := inv.Context()
144+
145+
org, err := orgContext.Selected(inv, client)
146+
if err != nil {
147+
return xerrors.Errorf("current organization: %w", err)
148+
}
149+
150+
_, err = cliui.Prompt(inv, cliui.PromptOptions{
151+
Text: fmt.Sprintf("Are you sure you want to delete provisioner key %s?", pretty.Sprint(cliui.DefaultStyles.Keyword, inv.Args[0])),
152+
IsConfirm: true,
153+
})
154+
if err != nil {
155+
return err
156+
}
157+
158+
err = client.DeleteProvisionerKey(ctx, org.ID, inv.Args[0])
159+
if err != nil {
160+
return xerrors.Errorf("delete provisioner key: %w", err)
161+
}
162+
163+
_, _ = fmt.Fprintf(inv.Stdout, "Successfully deleted provisioner key %s!\n", pretty.Sprint(cliui.DefaultStyles.Keyword, strings.ToLower(inv.Args[0])))
164+
165+
return nil
166+
},
167+
}
168+
169+
cmd.Options = serpent.OptionSet{
170+
cliui.SkipPromptOption(),
171+
}
172+
orgContext.AttachOptions(cmd)
173+
174+
return cmd
175+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package cli_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/google/uuid"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/v2/cli/clitest"
11+
"github.com/coder/coder/v2/coderd/coderdtest"
12+
"github.com/coder/coder/v2/coderd/rbac"
13+
"github.com/coder/coder/v2/codersdk"
14+
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
15+
"github.com/coder/coder/v2/enterprise/coderd/license"
16+
"github.com/coder/coder/v2/pty/ptytest"
17+
"github.com/coder/coder/v2/testutil"
18+
)
19+
20+
func TestProvisionerKeys(t *testing.T) {
21+
t.Parallel()
22+
23+
t.Run("CRUD", func(t *testing.T) {
24+
t.Parallel()
25+
26+
dv := coderdtest.DeploymentValues(t)
27+
dv.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
28+
client, owner := coderdenttest.New(t, &coderdenttest.Options{
29+
Options: &coderdtest.Options{
30+
DeploymentValues: dv,
31+
},
32+
LicenseOptions: &coderdenttest.LicenseOptions{
33+
Features: license.Features{
34+
codersdk.FeatureMultipleOrganizations: 1,
35+
},
36+
},
37+
})
38+
orgAdminClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
39+
40+
name := "dont-TEST-me"
41+
ctx := testutil.Context(t, testutil.WaitMedium)
42+
inv, conf := newCLI(
43+
t,
44+
"provisioner", "keys", "create", name,
45+
)
46+
47+
pty := ptytest.New(t)
48+
inv.Stdout = pty.Output()
49+
clitest.SetupConfig(t, orgAdminClient, conf)
50+
51+
err := inv.WithContext(ctx).Run()
52+
require.NoError(t, err)
53+
54+
line := pty.ReadLine(ctx)
55+
require.Contains(t, line, "Successfully created provisioner key")
56+
require.Contains(t, line, strings.ToLower(name))
57+
// empty line
58+
_ = pty.ReadLine(ctx)
59+
key := pty.ReadLine(ctx)
60+
require.NotEmpty(t, key)
61+
parts := strings.Split(key, ":")
62+
require.Len(t, parts, 2, "expected 2 parts")
63+
_, err = uuid.Parse(parts[0])
64+
require.NoError(t, err, "expected token to be a uuid")
65+
66+
inv, conf = newCLI(
67+
t,
68+
"provisioner", "keys", "ls",
69+
)
70+
pty = ptytest.New(t)
71+
inv.Stdout = pty.Output()
72+
clitest.SetupConfig(t, orgAdminClient, conf)
73+
74+
err = inv.WithContext(ctx).Run()
75+
require.NoError(t, err)
76+
line = pty.ReadLine(ctx)
77+
require.Contains(t, line, "NAME")
78+
require.Contains(t, line, "CREATED AT")
79+
require.Contains(t, line, "ORGANIZATION ID")
80+
line = pty.ReadLine(ctx)
81+
require.Contains(t, line, strings.ToLower(name))
82+
83+
inv, conf = newCLI(
84+
t,
85+
"provisioner", "keys", "delete", "-y", name,
86+
)
87+
88+
pty = ptytest.New(t)
89+
inv.Stdout = pty.Output()
90+
clitest.SetupConfig(t, orgAdminClient, conf)
91+
92+
err = inv.WithContext(ctx).Run()
93+
require.NoError(t, err)
94+
line = pty.ReadLine(ctx)
95+
require.Contains(t, line, "Successfully deleted provisioner key")
96+
require.Contains(t, line, strings.ToLower(name))
97+
98+
inv, conf = newCLI(
99+
t,
100+
"provisioner", "keys", "ls",
101+
)
102+
pty = ptytest.New(t)
103+
inv.Stdout = pty.Output()
104+
clitest.SetupConfig(t, orgAdminClient, conf)
105+
106+
err = inv.WithContext(ctx).Run()
107+
require.NoError(t, err)
108+
line = pty.ReadLine(ctx)
109+
require.Contains(t, line, "No provisioner keys found")
110+
})
111+
}

enterprise/cli/testdata/coder_provisionerd_--help.golden

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ USAGE:
55

66
Manage provisioner daemons
77

8+
Aliases: provisioner
9+
810
SUBCOMMANDS:
911
start Run a provisioner daemon
1012

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder provisionerd keys
5+
6+
Manage provisioner keys
7+
8+
Aliases: key
9+
10+
SUBCOMMANDS:
11+
create Create a new provisioner key
12+
delete Delete a provisioner key
13+
list List provisioner keys
14+
15+
———
16+
Run `coder --help` for a list of global options.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder provisionerd keys create [flags] <name>
5+
6+
Create a new provisioner key
7+
8+
OPTIONS:
9+
-O, --org string, $CODER_ORGANIZATION
10+
Select which organization (uuid or name) to use.
11+
12+
———
13+
Run `coder --help` for a list of global options.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder provisionerd keys delete [flags] <name>
5+
6+
Delete a provisioner key
7+
8+
Aliases: rm
9+
10+
OPTIONS:
11+
-O, --org string, $CODER_ORGANIZATION
12+
Select which organization (uuid or name) to use.
13+
14+
-y, --yes bool
15+
Bypass prompts.
16+
17+
———
18+
Run `coder --help` for a list of global options.

0 commit comments

Comments
 (0)