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

Add secrets ls, add, view, rm commands #81

Merged
merged 2 commits into from
Jul 30, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add secrets ls, add, view, rm commands
  • Loading branch information
cmoog committed Jul 30, 2020
commit 01e9b7785af9735b3a65b5bcfd979676e34014a9
14 changes: 4 additions & 10 deletions cmd/coder/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,21 @@ package main
import (
"net/url"

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

"cdr.dev/coder-cli/internal/config"
"cdr.dev/coder-cli/internal/entclient"
)

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

rawURL, err := config.URL.Read()
if err != nil {
flog.Fatal("read url: %v (did you run coder login?)", err)
}
xcli.RequireSuccess(err, "read url: %v (did you run coder login?)", err)

u, err := url.Parse(rawURL)
if err != nil {
flog.Fatal("url misformatted: %v (try runing coder login)", err)
}
xcli.RequireSuccess(err, "url misformatted: %v (try runing coder login)", err)

return &entclient.Client{
BaseURL: u,
Expand Down
15 changes: 6 additions & 9 deletions cmd/coder/ceapi.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"cdr.dev/coder-cli/internal/xcli"

"go.coder.com/flog"

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

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

orgs = userOrgs(me, orgs)

var allEnvs []entclient.Environment

for _, org := range orgs {
envs, err := client.Envs(me, org)
if err != nil {
flog.Fatal("get envs for %v: %+v", org.Name, err)
}
xcli.RequireSuccess(err, "get envs for %v: %+v", org.Name, err)

for _, env := range envs {
allEnvs = append(allEnvs, env)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/coder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (r *rootCmd) Subcommands() []cli.Command {
&versionCmd{},
&configSSHCmd{},
&usersCmd{},
&secretsCmd{},
}
}

Expand Down
167 changes: 167 additions & 0 deletions cmd/coder/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package main

import (
"fmt"
"os"

"cdr.dev/coder-cli/internal/entclient"
"cdr.dev/coder-cli/internal/xcli"
"github.com/spf13/pflag"
"golang.org/x/xerrors"

"go.coder.com/flog"

"go.coder.com/cli"
)

var (
_ cli.FlaggedCommand = secretsCmd{}
_ cli.ParentCommand = secretsCmd{}

_ cli.FlaggedCommand = &listSecretsCmd{}
_ cli.FlaggedCommand = &addSecretCmd{}
)

type secretsCmd struct {
}

func (cmd secretsCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "secrets",
Desc: "interact with secrets owned by the authenticated user",
}
}

func (cmd secretsCmd) Run(fl *pflag.FlagSet) {
exitUsage(fl)
}

func (cmd secretsCmd) RegisterFlags(fl *pflag.FlagSet) {}

func (cmd secretsCmd) Subcommands() []cli.Command {
return []cli.Command{
&listSecretsCmd{},
&viewSecretsCmd{},
&addSecretCmd{},
&deleteSecretsCmd{},
}
}

type listSecretsCmd struct{}

func (cmd listSecretsCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "ls",
Desc: "list all secrets owned by the authenticated user",
}
}

func (cmd listSecretsCmd) Run(fl *pflag.FlagSet) {
client := requireAuth()

secrets, err := client.Secrets()
xcli.RequireSuccess(err, "failed to get secrets: %v", err)

w := xcli.HumanReadableWriter()
if len(secrets) > 0 {
_, err := fmt.Fprintln(w, xcli.TabDelimitedStructHeaders(secrets[0]))
xcli.RequireSuccess(err, "failed to write: %v", err)
}
for _, s := range secrets {
s.Value = "******" // value is omitted from bulk responses

_, err = fmt.Fprintln(w, xcli.TabDelimitedStructValues(s))
xcli.RequireSuccess(err, "failed to write: %v", err)
}
err = w.Flush()
xcli.RequireSuccess(err, "failed to flush writer: %v", err)
}

func (cmd *listSecretsCmd) RegisterFlags(fl *pflag.FlagSet) {}

type viewSecretsCmd struct{}

func (cmd viewSecretsCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "view",
Usage: "[secret_name]",
Desc: "view a secret owned by the authenticated user",
}
}

func (cmd viewSecretsCmd) Run(fl *pflag.FlagSet) {
var (
client = requireAuth()
name = fl.Arg(0)
)

secret, err := client.SecretByName(name)
xcli.RequireSuccess(err, "failed to get secret by name: %v", err)

_, err = fmt.Fprintln(os.Stdout, secret.Value)
xcli.RequireSuccess(err, "failed to write: %v", err)
}

type addSecretCmd struct {
name, value, description string
}

func (cmd *addSecretCmd) Validate() (e []error) {
if cmd.name == "" {
e = append(e, xerrors.New("--name is a required flag"))
}
if cmd.value == "" {
e = append(e, xerrors.New("--value is a required flag"))
}
return e
}

func (cmd *addSecretCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "add",
Usage: `--name MYSQL_KEY --value 123456 --description "MySQL credential for database access"`,
Desc: "insert a new secret",
}
}

func (cmd *addSecretCmd) Run(fl *pflag.FlagSet) {
var (
client = requireAuth()
)
xcli.Validate(cmd)

err := client.InsertSecret(entclient.InsertSecretReq{
Name: cmd.name,
Value: cmd.value,
Description: cmd.description,
})
xcli.RequireSuccess(err, "failed to insert secret: %v", err)
}

func (cmd *addSecretCmd) RegisterFlags(fl *pflag.FlagSet) {
fl.StringVar(&cmd.name, "name", "", "the name of the secret")
fl.StringVar(&cmd.value, "value", "", "the value of the secret")
fl.StringVar(&cmd.description, "description", "", "a description of the secret")
}

type deleteSecretsCmd struct{}

func (cmd *deleteSecretsCmd) Spec() cli.CommandSpec {
return cli.CommandSpec{
Name: "rm",
Usage: "[secret_name]",
Desc: "remove a secret by name",
}
}

func (cmd *deleteSecretsCmd) Run(fl *pflag.FlagSet) {
var (
client = requireAuth()
name = fl.Arg(0)
)

err := client.DeleteSecretByName(name)
xcli.RequireSuccess(err, "failed to delete secret: %v", err)

flog.Info("Successfully deleted secret %q", name)
}
38 changes: 11 additions & 27 deletions cmd/coder/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import (
"encoding/json"
"fmt"
"os"
"reflect"
"strings"
"text/tabwriter"

"cdr.dev/coder-cli/internal/xcli"
"github.com/spf13/pflag"

"go.coder.com/cli"
"go.coder.com/flog"
)

type usersCmd struct {
Expand Down Expand Up @@ -39,41 +36,28 @@ type listCmd struct {
outputFmt string
}

func tabDelimited(data interface{}) string {
v := reflect.ValueOf(data)
s := &strings.Builder{}
for i := 0; i < v.NumField(); i++ {
s.WriteString(fmt.Sprintf("%s\t", v.Field(i).Interface()))
}
return s.String()
}

func (cmd *listCmd) Run(fl *pflag.FlagSet) {
entClient := requireAuth()

users, err := entClient.Users()
if err != nil {
flog.Fatal("failed to get users: %v", err)
}
xcli.RequireSuccess(err, "failed to get users: %v", err)

switch cmd.outputFmt {
case "human":
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
w := xcli.HumanReadableWriter()
if len(users) > 0 {
_, err = fmt.Fprintln(w, xcli.TabDelimitedStructHeaders(users[0]))
xcli.RequireSuccess(err, "failed to write: %v", err)
}
for _, u := range users {
_, err = fmt.Fprintln(w, tabDelimited(u))
if err != nil {
flog.Fatal("failed to write: %v", err)
}
_, err = fmt.Fprintln(w, xcli.TabDelimitedStructValues(u))
xcli.RequireSuccess(err, "failed to write: %v", err)
}
err = w.Flush()
if err != nil {
flog.Fatal("failed to flush writer: %v", err)
}
xcli.RequireSuccess(err, "failed to flush writer: %v", err)
case "json":
err = json.NewEncoder(os.Stdout).Encode(users)
if err != nil {
flog.Fatal("failed to encode users to json: %v", err)
}
xcli.RequireSuccess(err, "failed to encode users to json: %v", err)
default:
exitUsage(fl)
}
Expand Down
3 changes: 3 additions & 0 deletions internal/entclient/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"golang.org/x/xerrors"
)

// ErrNotFound describes an error case in which the request resource could not be found
var ErrNotFound = xerrors.Errorf("resource not found")

func bodyError(resp *http.Response) error {
byt, err := httputil.DumpResponse(resp, false)
if err != nil {
Expand Down
74 changes: 74 additions & 0 deletions internal/entclient/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package entclient

import (
"net/http"
"time"
)

// Secret describes a Coder secret
type Secret struct {
ID string `json:"id"`
Name string `json:"name"`
Value string `json:"value,omitempty"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// Secrets gets all secrets owned by the authed user
func (c *Client) Secrets() ([]Secret, error) {
var secrets []Secret
err := c.requestBody(http.MethodGet, "/api/users/me/secrets", nil, &secrets)
return secrets, err
}

func (c *Client) secretByID(id string) (*Secret, error) {
var secret Secret
err := c.requestBody(http.MethodGet, "/api/users/me/secrets/"+id, nil, &secret)
return &secret, err
}

func (c *Client) secretNameToID(name string) (id string, _ error) {
secrets, err := c.Secrets()
if err != nil {
return "", err
}
for _, s := range secrets {
if s.Name == name {
return s.ID, nil
}
}
return "", ErrNotFound
}

// SecretByName gets a secret object by name
func (c *Client) SecretByName(name string) (*Secret, error) {
id, err := c.secretNameToID(name)
if err != nil {
return nil, err
}
return c.secretByID(id)
}

// InsertSecretReq describes the request body for creating a new secret
type InsertSecretReq struct {
Name string `json:"name"`
Value string `json:"value"`
Description string `json:"description"`
}

// InsertSecret adds a new secret for the authed user
func (c *Client) InsertSecret(req InsertSecretReq) error {
_, err := c.request(http.MethodPost, "/api/users/me/secrets", req)
return err
}

// DeleteSecretByName deletes the authenticated users secret with the given name
func (c *Client) DeleteSecretByName(name string) error {
id, err := c.secretNameToID(name)
if err != nil {
return nil
}
_, err = c.request(http.MethodDelete, "/api/users/me/secrets/"+id, nil)
return err
}
Loading