Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 01e9b77

Browse files
committed
Add secrets ls, add, view, rm commands
1 parent 87c708a commit 01e9b77

File tree

9 files changed

+405
-46
lines changed

9 files changed

+405
-46
lines changed

Diff for: cmd/coder/auth.go

+4-10
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,21 @@ package main
33
import (
44
"net/url"
55

6-
"go.coder.com/flog"
6+
"cdr.dev/coder-cli/internal/xcli"
77

88
"cdr.dev/coder-cli/internal/config"
99
"cdr.dev/coder-cli/internal/entclient"
1010
)
1111

1212
func requireAuth() *entclient.Client {
1313
sessionToken, err := config.Session.Read()
14-
if err != nil {
15-
flog.Fatal("read session: %v (did you run coder login?)", err)
16-
}
14+
xcli.RequireSuccess(err, "read session: %v (did you run coder login?)", err)
1715

1816
rawURL, err := config.URL.Read()
19-
if err != nil {
20-
flog.Fatal("read url: %v (did you run coder login?)", err)
21-
}
17+
xcli.RequireSuccess(err, "read url: %v (did you run coder login?)", err)
2218

2319
u, err := url.Parse(rawURL)
24-
if err != nil {
25-
flog.Fatal("url misformatted: %v (try runing coder login)", err)
26-
}
20+
xcli.RequireSuccess(err, "url misformatted: %v (try runing coder login)", err)
2721

2822
return &entclient.Client{
2923
BaseURL: u,

Diff for: cmd/coder/ceapi.go

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"cdr.dev/coder-cli/internal/xcli"
5+
46
"go.coder.com/flog"
57

68
"cdr.dev/coder-cli/internal/entclient"
@@ -27,24 +29,19 @@ outer:
2729
// getEnvs returns all environments for the user.
2830
func getEnvs(client *entclient.Client) []entclient.Environment {
2931
me, err := client.Me()
30-
if err != nil {
31-
flog.Fatal("get self: %+v", err)
32-
}
32+
xcli.RequireSuccess(err, "get self: %+v", err)
3333

3434
orgs, err := client.Orgs()
35-
if err != nil {
36-
flog.Fatal("get orgs: %+v", err)
37-
}
35+
xcli.RequireSuccess(err, "get orgs: %+v", err)
3836

3937
orgs = userOrgs(me, orgs)
4038

4139
var allEnvs []entclient.Environment
4240

4341
for _, org := range orgs {
4442
envs, err := client.Envs(me, org)
45-
if err != nil {
46-
flog.Fatal("get envs for %v: %+v", org.Name, err)
47-
}
43+
xcli.RequireSuccess(err, "get envs for %v: %+v", org.Name, err)
44+
4845
for _, env := range envs {
4946
allEnvs = append(allEnvs, env)
5047
}

Diff for: cmd/coder/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func (r *rootCmd) Subcommands() []cli.Command {
4343
&versionCmd{},
4444
&configSSHCmd{},
4545
&usersCmd{},
46+
&secretsCmd{},
4647
}
4748
}
4849

Diff for: cmd/coder/secrets.go

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"cdr.dev/coder-cli/internal/entclient"
8+
"cdr.dev/coder-cli/internal/xcli"
9+
"github.com/spf13/pflag"
10+
"golang.org/x/xerrors"
11+
12+
"go.coder.com/flog"
13+
14+
"go.coder.com/cli"
15+
)
16+
17+
var (
18+
_ cli.FlaggedCommand = secretsCmd{}
19+
_ cli.ParentCommand = secretsCmd{}
20+
21+
_ cli.FlaggedCommand = &listSecretsCmd{}
22+
_ cli.FlaggedCommand = &addSecretCmd{}
23+
)
24+
25+
type secretsCmd struct {
26+
}
27+
28+
func (cmd secretsCmd) Spec() cli.CommandSpec {
29+
return cli.CommandSpec{
30+
Name: "secrets",
31+
Desc: "interact with secrets owned by the authenticated user",
32+
}
33+
}
34+
35+
func (cmd secretsCmd) Run(fl *pflag.FlagSet) {
36+
exitUsage(fl)
37+
}
38+
39+
func (cmd secretsCmd) RegisterFlags(fl *pflag.FlagSet) {}
40+
41+
func (cmd secretsCmd) Subcommands() []cli.Command {
42+
return []cli.Command{
43+
&listSecretsCmd{},
44+
&viewSecretsCmd{},
45+
&addSecretCmd{},
46+
&deleteSecretsCmd{},
47+
}
48+
}
49+
50+
type listSecretsCmd struct{}
51+
52+
func (cmd listSecretsCmd) Spec() cli.CommandSpec {
53+
return cli.CommandSpec{
54+
Name: "ls",
55+
Desc: "list all secrets owned by the authenticated user",
56+
}
57+
}
58+
59+
func (cmd listSecretsCmd) Run(fl *pflag.FlagSet) {
60+
client := requireAuth()
61+
62+
secrets, err := client.Secrets()
63+
xcli.RequireSuccess(err, "failed to get secrets: %v", err)
64+
65+
w := xcli.HumanReadableWriter()
66+
if len(secrets) > 0 {
67+
_, err := fmt.Fprintln(w, xcli.TabDelimitedStructHeaders(secrets[0]))
68+
xcli.RequireSuccess(err, "failed to write: %v", err)
69+
}
70+
for _, s := range secrets {
71+
s.Value = "******" // value is omitted from bulk responses
72+
73+
_, err = fmt.Fprintln(w, xcli.TabDelimitedStructValues(s))
74+
xcli.RequireSuccess(err, "failed to write: %v", err)
75+
}
76+
err = w.Flush()
77+
xcli.RequireSuccess(err, "failed to flush writer: %v", err)
78+
}
79+
80+
func (cmd *listSecretsCmd) RegisterFlags(fl *pflag.FlagSet) {}
81+
82+
type viewSecretsCmd struct{}
83+
84+
func (cmd viewSecretsCmd) Spec() cli.CommandSpec {
85+
return cli.CommandSpec{
86+
Name: "view",
87+
Usage: "[secret_name]",
88+
Desc: "view a secret owned by the authenticated user",
89+
}
90+
}
91+
92+
func (cmd viewSecretsCmd) Run(fl *pflag.FlagSet) {
93+
var (
94+
client = requireAuth()
95+
name = fl.Arg(0)
96+
)
97+
98+
secret, err := client.SecretByName(name)
99+
xcli.RequireSuccess(err, "failed to get secret by name: %v", err)
100+
101+
_, err = fmt.Fprintln(os.Stdout, secret.Value)
102+
xcli.RequireSuccess(err, "failed to write: %v", err)
103+
}
104+
105+
type addSecretCmd struct {
106+
name, value, description string
107+
}
108+
109+
func (cmd *addSecretCmd) Validate() (e []error) {
110+
if cmd.name == "" {
111+
e = append(e, xerrors.New("--name is a required flag"))
112+
}
113+
if cmd.value == "" {
114+
e = append(e, xerrors.New("--value is a required flag"))
115+
}
116+
return e
117+
}
118+
119+
func (cmd *addSecretCmd) Spec() cli.CommandSpec {
120+
return cli.CommandSpec{
121+
Name: "add",
122+
Usage: `--name MYSQL_KEY --value 123456 --description "MySQL credential for database access"`,
123+
Desc: "insert a new secret",
124+
}
125+
}
126+
127+
func (cmd *addSecretCmd) Run(fl *pflag.FlagSet) {
128+
var (
129+
client = requireAuth()
130+
)
131+
xcli.Validate(cmd)
132+
133+
err := client.InsertSecret(entclient.InsertSecretReq{
134+
Name: cmd.name,
135+
Value: cmd.value,
136+
Description: cmd.description,
137+
})
138+
xcli.RequireSuccess(err, "failed to insert secret: %v", err)
139+
}
140+
141+
func (cmd *addSecretCmd) RegisterFlags(fl *pflag.FlagSet) {
142+
fl.StringVar(&cmd.name, "name", "", "the name of the secret")
143+
fl.StringVar(&cmd.value, "value", "", "the value of the secret")
144+
fl.StringVar(&cmd.description, "description", "", "a description of the secret")
145+
}
146+
147+
type deleteSecretsCmd struct{}
148+
149+
func (cmd *deleteSecretsCmd) Spec() cli.CommandSpec {
150+
return cli.CommandSpec{
151+
Name: "rm",
152+
Usage: "[secret_name]",
153+
Desc: "remove a secret by name",
154+
}
155+
}
156+
157+
func (cmd *deleteSecretsCmd) Run(fl *pflag.FlagSet) {
158+
var (
159+
client = requireAuth()
160+
name = fl.Arg(0)
161+
)
162+
163+
err := client.DeleteSecretByName(name)
164+
xcli.RequireSuccess(err, "failed to delete secret: %v", err)
165+
166+
flog.Info("Successfully deleted secret %q", name)
167+
}

Diff for: cmd/coder/users.go

+11-27
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7-
"reflect"
8-
"strings"
9-
"text/tabwriter"
107

8+
"cdr.dev/coder-cli/internal/xcli"
119
"github.com/spf13/pflag"
1210

1311
"go.coder.com/cli"
14-
"go.coder.com/flog"
1512
)
1613

1714
type usersCmd struct {
@@ -39,41 +36,28 @@ type listCmd struct {
3936
outputFmt string
4037
}
4138

42-
func tabDelimited(data interface{}) string {
43-
v := reflect.ValueOf(data)
44-
s := &strings.Builder{}
45-
for i := 0; i < v.NumField(); i++ {
46-
s.WriteString(fmt.Sprintf("%s\t", v.Field(i).Interface()))
47-
}
48-
return s.String()
49-
}
50-
5139
func (cmd *listCmd) Run(fl *pflag.FlagSet) {
5240
entClient := requireAuth()
5341

5442
users, err := entClient.Users()
55-
if err != nil {
56-
flog.Fatal("failed to get users: %v", err)
57-
}
43+
xcli.RequireSuccess(err, "failed to get users: %v", err)
5844

5945
switch cmd.outputFmt {
6046
case "human":
61-
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
47+
w := xcli.HumanReadableWriter()
48+
if len(users) > 0 {
49+
_, err = fmt.Fprintln(w, xcli.TabDelimitedStructHeaders(users[0]))
50+
xcli.RequireSuccess(err, "failed to write: %v", err)
51+
}
6252
for _, u := range users {
63-
_, err = fmt.Fprintln(w, tabDelimited(u))
64-
if err != nil {
65-
flog.Fatal("failed to write: %v", err)
66-
}
53+
_, err = fmt.Fprintln(w, xcli.TabDelimitedStructValues(u))
54+
xcli.RequireSuccess(err, "failed to write: %v", err)
6755
}
6856
err = w.Flush()
69-
if err != nil {
70-
flog.Fatal("failed to flush writer: %v", err)
71-
}
57+
xcli.RequireSuccess(err, "failed to flush writer: %v", err)
7258
case "json":
7359
err = json.NewEncoder(os.Stdout).Encode(users)
74-
if err != nil {
75-
flog.Fatal("failed to encode users to json: %v", err)
76-
}
60+
xcli.RequireSuccess(err, "failed to encode users to json: %v", err)
7761
default:
7862
exitUsage(fl)
7963
}

Diff for: internal/entclient/error.go

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"golang.org/x/xerrors"
88
)
99

10+
// ErrNotFound describes an error case in which the request resource could not be found
11+
var ErrNotFound = xerrors.Errorf("resource not found")
12+
1013
func bodyError(resp *http.Response) error {
1114
byt, err := httputil.DumpResponse(resp, false)
1215
if err != nil {

Diff for: internal/entclient/secrets.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package entclient
2+
3+
import (
4+
"net/http"
5+
"time"
6+
)
7+
8+
// Secret describes a Coder secret
9+
type Secret struct {
10+
ID string `json:"id"`
11+
Name string `json:"name"`
12+
Value string `json:"value,omitempty"`
13+
Description string `json:"description"`
14+
CreatedAt time.Time `json:"created_at"`
15+
UpdatedAt time.Time `json:"updated_at"`
16+
}
17+
18+
// Secrets gets all secrets owned by the authed user
19+
func (c *Client) Secrets() ([]Secret, error) {
20+
var secrets []Secret
21+
err := c.requestBody(http.MethodGet, "/api/users/me/secrets", nil, &secrets)
22+
return secrets, err
23+
}
24+
25+
func (c *Client) secretByID(id string) (*Secret, error) {
26+
var secret Secret
27+
err := c.requestBody(http.MethodGet, "/api/users/me/secrets/"+id, nil, &secret)
28+
return &secret, err
29+
}
30+
31+
func (c *Client) secretNameToID(name string) (id string, _ error) {
32+
secrets, err := c.Secrets()
33+
if err != nil {
34+
return "", err
35+
}
36+
for _, s := range secrets {
37+
if s.Name == name {
38+
return s.ID, nil
39+
}
40+
}
41+
return "", ErrNotFound
42+
}
43+
44+
// SecretByName gets a secret object by name
45+
func (c *Client) SecretByName(name string) (*Secret, error) {
46+
id, err := c.secretNameToID(name)
47+
if err != nil {
48+
return nil, err
49+
}
50+
return c.secretByID(id)
51+
}
52+
53+
// InsertSecretReq describes the request body for creating a new secret
54+
type InsertSecretReq struct {
55+
Name string `json:"name"`
56+
Value string `json:"value"`
57+
Description string `json:"description"`
58+
}
59+
60+
// InsertSecret adds a new secret for the authed user
61+
func (c *Client) InsertSecret(req InsertSecretReq) error {
62+
_, err := c.request(http.MethodPost, "/api/users/me/secrets", req)
63+
return err
64+
}
65+
66+
// DeleteSecretByName deletes the authenticated users secret with the given name
67+
func (c *Client) DeleteSecretByName(name string) error {
68+
id, err := c.secretNameToID(name)
69+
if err != nil {
70+
return nil
71+
}
72+
_, err = c.request(http.MethodDelete, "/api/users/me/secrets/"+id, nil)
73+
return err
74+
}

0 commit comments

Comments
 (0)