Skip to content

Commit f975701

Browse files
authored
feat: add provisioner key cli commands (#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy