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

cmd/coder/auth.go

Lines changed: 4 additions & 10 deletions
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,

cmd/coder/ceapi.go

Lines changed: 6 additions & 9 deletions
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
}

cmd/coder/main.go

Lines changed: 1 addition & 0 deletions
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

cmd/coder/secrets.go

Lines changed: 167 additions & 0 deletions
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+
}

cmd/coder/users.go

Lines changed: 11 additions & 27 deletions
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
}

internal/entclient/error.go

Lines changed: 3 additions & 0 deletions
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 {

internal/entclient/secrets.go

Lines changed: 74 additions & 0 deletions
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)
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