diff --git a/ci/integration/integration_test.go b/ci/integration/integration_test.go index a452cd18..26f510f9 100644 --- a/ci/integration/integration_test.go +++ b/ci/integration/integration_test.go @@ -4,10 +4,8 @@ import ( "context" "encoding/json" "fmt" - "os" - "os/exec" - "path/filepath" - "strings" + "math/rand" + "regexp" "testing" "time" @@ -17,50 +15,6 @@ import ( "cdr.dev/slog/sloggers/slogtest/assert" ) -func build(path string) error { - cmd := exec.Command( - "sh", "-c", - fmt.Sprintf("cd ../../ && go build -o %s ./cmd/coder", path), - ) - cmd.Env = append(os.Environ(), "GOOS=linux", "CGO_ENABLED=0") - - _, err := cmd.CombinedOutput() - if err != nil { - return err - } - return nil -} - -var binpath string - -func init() { - cwd, err := os.Getwd() - if err != nil { - panic(err) - } - - binpath = filepath.Join(cwd, "bin", "coder") - err = build(binpath) - if err != nil { - panic(err) - } -} - -// write session tokens to the given container runner -func headlessLogin(ctx context.Context, t *testing.T, runner *tcli.ContainerRunner) { - creds := login(ctx, t) - cmd := exec.CommandContext(ctx, "sh", "-c", "mkdir -p ~/.config/coder && cat > ~/.config/coder/session") - - // !IMPORTANT: be careful that this does not appear in logs - cmd.Stdin = strings.NewReader(creds.token) - runner.RunCmd(cmd).Assert(t, - tcli.Success(), - ) - runner.Run(ctx, fmt.Sprintf("echo -ne %s > ~/.config/coder/url", creds.url)).Assert(t, - tcli.Success(), - ) -} - func TestCoderCLI(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) @@ -116,7 +70,7 @@ func TestCoderCLI(t *testing.T) { var user entclient.User c.Run(ctx, `coder users ls -o json | jq -c '.[] | select( .username == "charlie")'`).Assert(t, tcli.Success(), - jsonUnmarshals(&user), + stdoutUnmarshalsJSON(&user), ) assert.Equal(t, "user email is as expected", "charlie@coder.com", user.Email) assert.Equal(t, "username is as expected", "Charlie", user.Name) @@ -135,10 +89,80 @@ func TestCoderCLI(t *testing.T) { ) } -func jsonUnmarshals(target interface{}) tcli.Assertion { +func TestSecrets(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + + c, err := tcli.NewContainerRunner(ctx, &tcli.ContainerConfig{ + Image: "codercom/enterprise-dev", + Name: "secrets-cli-tests", + BindMounts: map[string]string{ + binpath: "/bin/coder", + }, + }) + assert.Success(t, "new run container", err) + defer c.Close() + + headlessLogin(ctx, t, c) + + c.Run(ctx, "coder secrets ls").Assert(t, + tcli.Success(), + ) + + name, value := randString(8), randString(8) + + c.Run(ctx, "coder secrets create").Assert(t, + tcli.Error(), + tcli.StdoutEmpty(), + tcli.StderrMatches("required flag"), + ) + + c.Run(ctx, fmt.Sprintf("coder secrets create --name %s --value %s", name, value)).Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + ) + + c.Run(ctx, "coder secrets ls").Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches("Value"), + tcli.StdoutMatches(regexp.QuoteMeta(name)), + ) + + c.Run(ctx, "coder secrets view "+name).Assert(t, + tcli.Success(), + tcli.StderrEmpty(), + tcli.StdoutMatches(regexp.QuoteMeta(value)), + ) + + c.Run(ctx, "coder secrets rm").Assert(t, + tcli.Error(), + ) + c.Run(ctx, "coder secrets rm "+name).Assert(t, + tcli.Success(), + ) + c.Run(ctx, "coder secrets view "+name).Assert(t, + tcli.Error(), + tcli.StdoutEmpty(), + ) +} + +func stdoutUnmarshalsJSON(target interface{}) tcli.Assertion { return func(t *testing.T, r *tcli.CommandResult) { slog.Helper() err := json.Unmarshal(r.Stdout, target) assert.Success(t, "json unmarshals", err) } } + +var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) + +func randString(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} diff --git a/ci/integration/setup_test.go b/ci/integration/setup_test.go new file mode 100644 index 00000000..9ae69c29 --- /dev/null +++ b/ci/integration/setup_test.go @@ -0,0 +1,60 @@ +package integration + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "cdr.dev/coder-cli/ci/tcli" + "golang.org/x/xerrors" +) + +var binpath string + +// initialize integration tests by building the coder-cli binary +func init() { + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + + binpath = filepath.Join(cwd, "bin", "coder") + err = build(binpath) + if err != nil { + panic(err) + } +} + +// build the coder-cli binary and move to the integration testing bin directory +func build(path string) error { + cmd := exec.Command( + "sh", "-c", + fmt.Sprintf("cd ../../ && go build -o %s ./cmd/coder", path), + ) + cmd.Env = append(os.Environ(), "GOOS=linux", "CGO_ENABLED=0") + + out, err := cmd.CombinedOutput() + if err != nil { + return xerrors.Errorf("failed to build coder-cli (%v): %w", string(out), err) + } + return nil +} + +// write session tokens to the given container runner +func headlessLogin(ctx context.Context, t *testing.T, runner *tcli.ContainerRunner) { + creds := login(ctx, t) + cmd := exec.CommandContext(ctx, "sh", "-c", "mkdir -p ~/.config/coder && cat > ~/.config/coder/session") + + // !IMPORTANT: be careful that this does not appear in logs + cmd.Stdin = strings.NewReader(creds.token) + runner.RunCmd(cmd).Assert(t, + tcli.Success(), + ) + runner.Run(ctx, fmt.Sprintf("echo -ne %s > ~/.config/coder/url", creds.url)).Assert(t, + tcli.Success(), + ) +} diff --git a/ci/tcli/tcli.go b/ci/tcli/tcli.go index 101cc926..8a9fd2be 100644 --- a/ci/tcli/tcli.go +++ b/ci/tcli/tcli.go @@ -163,13 +163,16 @@ type Assertable struct { } // Assert runs the Assertable and -func (a Assertable) Assert(t *testing.T, option ...Assertion) { +func (a *Assertable) Assert(t *testing.T, option ...Assertion) { slog.Helper() var ( stdout bytes.Buffer stderr bytes.Buffer result CommandResult ) + if a.cmd == nil { + slogtest.Fatal(t, "test failed to initialize: no command specified") + } a.cmd.Stdout = &stdout a.cmd.Stderr = &stderr diff --git a/cmd/coder/auth.go b/cmd/coder/auth.go index cf7a16b1..574a0c0b 100644 --- a/cmd/coder/auth.go +++ b/cmd/coder/auth.go @@ -3,27 +3,19 @@ package main import ( "net/url" - "go.coder.com/flog" - "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) - } + 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) - } + 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) - } + requireSuccess(err, "url misformatted: %v (try runing coder login)", err) return &entclient.Client{ BaseURL: u, diff --git a/cmd/coder/ceapi.go b/cmd/coder/ceapi.go index fd59046c..cd350f84 100644 --- a/cmd/coder/ceapi.go +++ b/cmd/coder/ceapi.go @@ -27,14 +27,10 @@ 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) - } + requireSuccess(err, "get self: %+v", err) orgs, err := client.Orgs() - if err != nil { - flog.Fatal("get orgs: %+v", err) - } + requireSuccess(err, "get orgs: %+v", err) orgs = userOrgs(me, orgs) @@ -42,9 +38,8 @@ func getEnvs(client *entclient.Client) []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) - } + requireSuccess(err, "get envs for %v: %+v", org.Name, err) + for _, env := range envs { allEnvs = append(allEnvs, env) } diff --git a/cmd/coder/main.go b/cmd/coder/main.go index 93ebf022..5680d30d 100644 --- a/cmd/coder/main.go +++ b/cmd/coder/main.go @@ -6,7 +6,7 @@ import ( _ "net/http/pprof" "os" - "cdr.dev/coder-cli/internal/xterminal" + "cdr.dev/coder-cli/internal/x/xterminal" "github.com/spf13/pflag" "go.coder.com/flog" @@ -43,6 +43,7 @@ func (r *rootCmd) Subcommands() []cli.Command { &versionCmd{}, &configSSHCmd{}, &usersCmd{}, + &secretsCmd{}, } } @@ -61,3 +62,10 @@ func main() { cli.RunRoot(&rootCmd{}) } + +// requireSuccess prints the given message and format args as a fatal error if err != nil +func requireSuccess(err error, msg string, args ...interface{}) { + if err != nil { + flog.Fatal(msg, args...) + } +} diff --git a/cmd/coder/secrets.go b/cmd/coder/secrets.go new file mode 100644 index 00000000..0fa9af52 --- /dev/null +++ b/cmd/coder/secrets.go @@ -0,0 +1,178 @@ +package main + +import ( + "fmt" + "os" + + "cdr.dev/coder-cli/internal/entclient" + "cdr.dev/coder-cli/internal/x/xtabwriter" + "cdr.dev/coder-cli/internal/x/xvalidate" + "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 = &createSecretCmd{} +) + +type secretsCmd struct { +} + +func (cmd secretsCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "secrets", + Usage: "[subcommand]", + Desc: "interact with secrets", + } +} + +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{}, + &createSecretCmd{}, + &deleteSecretsCmd{}, + } +} + +type listSecretsCmd struct{} + +func (cmd *listSecretsCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "ls", + Desc: "list all secrets", + } +} + +func (cmd *listSecretsCmd) Run(fl *pflag.FlagSet) { + client := requireAuth() + + secrets, err := client.Secrets() + requireSuccess(err, "failed to get secrets: %v", err) + + if len(secrets) < 1 { + flog.Info("No secrets found") + return + } + + w := xtabwriter.NewWriter() + _, err = fmt.Fprintln(w, xtabwriter.StructFieldNames(secrets[0])) + requireSuccess(err, "failed to write: %v", err) + for _, s := range secrets { + s.Value = "******" // value is omitted from bulk responses + + _, err = fmt.Fprintln(w, xtabwriter.StructValues(s)) + requireSuccess(err, "failed to write: %v", err) + } + err = w.Flush() + 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", + } +} + +func (cmd viewSecretsCmd) Run(fl *pflag.FlagSet) { + var ( + client = requireAuth() + name = fl.Arg(0) + ) + if name == "" { + exitUsage(fl) + } + + secret, err := client.SecretByName(name) + requireSuccess(err, "failed to get secret by name: %v", err) + + _, err = fmt.Fprintln(os.Stdout, secret.Value) + requireSuccess(err, "failed to write: %v", err) +} + +type createSecretCmd struct { + name, value, description string +} + +func (cmd *createSecretCmd) 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 *createSecretCmd) Spec() cli.CommandSpec { + return cli.CommandSpec{ + Name: "create", + Usage: `--name MYSQL_KEY --value 123456 --description "MySQL credential for database access"`, + Desc: "insert a new secret", + } +} + +func (cmd *createSecretCmd) Run(fl *pflag.FlagSet) { + var ( + client = requireAuth() + ) + xvalidate.Validate(cmd) + + err := client.InsertSecret(entclient.InsertSecretReq{ + Name: cmd.name, + Value: cmd.value, + Description: cmd.description, + }) + requireSuccess(err, "failed to insert secret: %v", err) +} + +func (cmd *createSecretCmd) 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", + } +} + +func (cmd *deleteSecretsCmd) Run(fl *pflag.FlagSet) { + var ( + client = requireAuth() + name = fl.Arg(0) + ) + if name == "" { + exitUsage(fl) + } + + err := client.DeleteSecretByName(name) + requireSuccess(err, "failed to delete secret: %v", err) + + flog.Info("Successfully deleted secret %q", name) +} diff --git a/cmd/coder/shell.go b/cmd/coder/shell.go index 7e8b70ef..c7b42564 100644 --- a/cmd/coder/shell.go +++ b/cmd/coder/shell.go @@ -17,7 +17,7 @@ import ( "go.coder.com/flog" "cdr.dev/coder-cli/internal/activity" - "cdr.dev/coder-cli/internal/xterminal" + "cdr.dev/coder-cli/internal/x/xterminal" "cdr.dev/wsep" ) diff --git a/cmd/coder/users.go b/cmd/coder/users.go index bef0d7c0..5c671704 100644 --- a/cmd/coder/users.go +++ b/cmd/coder/users.go @@ -4,14 +4,12 @@ import ( "encoding/json" "fmt" "os" - "reflect" - "strings" - "text/tabwriter" + "cdr.dev/coder-cli/internal/x/xtabwriter" + "cdr.dev/coder-cli/internal/x/xvalidate" "github.com/spf13/pflag" "go.coder.com/cli" - "go.coder.com/flog" ) type usersCmd struct { @@ -39,45 +37,32 @@ 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) { + xvalidate.Validate(cmd) entClient := requireAuth() users, err := entClient.Users() - if err != nil { - flog.Fatal("failed to get users: %v", err) - } + requireSuccess(err, "failed to get users: %v", err) switch cmd.outputFmt { case "human": - w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + w := xtabwriter.NewWriter() + if len(users) > 0 { + _, err = fmt.Fprintln(w, xtabwriter.StructFieldNames(users[0])) + 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, xtabwriter.StructValues(u)) + requireSuccess(err, "failed to write: %v", err) } err = w.Flush() - if err != nil { - flog.Fatal("failed to flush writer: %v", err) - } + 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) - } + requireSuccess(err, "failed to encode users to json: %v", err) default: exitUsage(fl) } - } func (cmd *listCmd) RegisterFlags(fl *pflag.FlagSet) { @@ -91,3 +76,10 @@ func (cmd *listCmd) Spec() cli.CommandSpec { Desc: "list all users", } } + +func (cmd *listCmd) Validate() (e []error) { + if !(cmd.outputFmt == "json" || cmd.outputFmt == "human") { + e = append(e, fmt.Errorf(`--output must be "json" or "human"`)) + } + return e +} diff --git a/internal/entclient/error.go b/internal/entclient/error.go index 49f58669..877085f2 100644 --- a/internal/entclient/error.go +++ b/internal/entclient/error.go @@ -1,16 +1,32 @@ package entclient import ( + "encoding/json" "net/http" "net/http/httputil" "golang.org/x/xerrors" ) +// ErrNotFound describes an error case in which the requested resource could not be found +var ErrNotFound = xerrors.Errorf("resource not found") + +type apiError struct { + Err struct { + Msg string `json:"msg,required"` + } `json:"error"` +} + func bodyError(resp *http.Response) error { byt, err := httputil.DumpResponse(resp, false) if err != nil { return xerrors.Errorf("dump response: %w", err) } - return xerrors.Errorf("%s\n%s", resp.Request.URL, byt) + + var msg apiError + err = json.NewDecoder(resp.Body).Decode(&msg) + if err != nil || msg.Err.Msg == "" { + return xerrors.Errorf("%s\n%s", resp.Request.URL, byt) + } + return xerrors.Errorf("%s\n%s%s", resp.Request.URL, byt, msg.Err.Msg) } diff --git a/internal/entclient/request.go b/internal/entclient/request.go index b5873f81..dfd0d6fe 100644 --- a/internal/entclient/request.go +++ b/internal/entclient/request.go @@ -39,7 +39,7 @@ func (c Client) requestBody( } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode > 299 { return bodyError(resp) } diff --git a/internal/entclient/secrets.go b/internal/entclient/secrets.go new file mode 100644 index 00000000..98fa543b --- /dev/null +++ b/internal/entclient/secrets.go @@ -0,0 +1,75 @@ +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 { + var resp interface{} + err := c.requestBody(http.MethodPost, "/api/users/me/secrets", req, &resp) + 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 err + } + _, err = c.request(http.MethodDelete, "/api/users/me/secrets/"+id, nil) + return err +} diff --git a/internal/x/xtabwriter/tabwriter.go b/internal/x/xtabwriter/tabwriter.go new file mode 100644 index 00000000..1c8b1167 --- /dev/null +++ b/internal/x/xtabwriter/tabwriter.go @@ -0,0 +1,34 @@ +package xtabwriter + +import ( + "fmt" + "os" + "reflect" + "strings" + "text/tabwriter" +) + +// NewWriter chooses reasonable defaults for a human readable output of tabular data +func NewWriter() *tabwriter.Writer { + return tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) +} + +// StructValues tab delimits the values of a given struct +func StructValues(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() +} + +// StructFieldNames tab delimits the field names of a given struct +func StructFieldNames(data interface{}) string { + v := reflect.ValueOf(data) + s := &strings.Builder{} + for i := 0; i < v.NumField(); i++ { + s.WriteString(fmt.Sprintf("%s\t", v.Type().Field(i).Name)) + } + return s.String() +} diff --git a/internal/xterminal/doc.go b/internal/x/xterminal/doc.go similarity index 100% rename from internal/xterminal/doc.go rename to internal/x/xterminal/doc.go diff --git a/internal/xterminal/terminal.go b/internal/x/xterminal/terminal.go similarity index 100% rename from internal/xterminal/terminal.go rename to internal/x/xterminal/terminal.go diff --git a/internal/xterminal/terminal_windows.go b/internal/x/xterminal/terminal_windows.go similarity index 100% rename from internal/xterminal/terminal_windows.go rename to internal/x/xterminal/terminal_windows.go diff --git a/internal/x/xvalidate/errors.go b/internal/x/xvalidate/errors.go new file mode 100644 index 00000000..70aec071 --- /dev/null +++ b/internal/x/xvalidate/errors.go @@ -0,0 +1,96 @@ +package xvalidate + +import ( + "bytes" + + "go.coder.com/flog" +) + +// cerrors contains a list of errors. +type cerrors struct { + cerrors []error +} + +func (e cerrors) writeTo(buf *bytes.Buffer) { + for i, err := range e.cerrors { + if err == nil { + continue + } + buf.WriteString(err.Error()) + // don't newline after last error + if i != len(e.cerrors)-1 { + buf.WriteRune('\n') + } + } +} + +func (e cerrors) Error() string { + buf := &bytes.Buffer{} + e.writeTo(buf) + return buf.String() +} + +// stripNils removes nil errors from the slice. +func stripNils(errs []error) []error { + // We can't range since errs may be resized + // during the loop. + for i := 0; i < len(errs); i++ { + err := errs[i] + if err == nil { + // shift down + copy(errs[i:], errs[i+1:]) + // pop off last element + errs = errs[:len(errs)-1] + } + } + return errs +} + +// flatten expands all parts of cerrors onto errs. +func flatten(errs []error) []error { + nerrs := make([]error, 0, len(errs)) + for _, err := range errs { + errs, ok := err.(cerrors) + if !ok { + nerrs = append(nerrs, err) + continue + } + nerrs = append(nerrs, errs.cerrors...) + } + return nerrs +} + +// combineErrors combines multiple errors into one +func combineErrors(errs ...error) error { + errs = stripNils(errs) + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + // Don't return if all of the errors of nil. + for _, err := range errs { + if err != nil { + return cerrors{cerrors: flatten(errs)} + } + } + return nil + } +} + +// Validator is a command capable of validating its flags +type Validator interface { + Validate() []error +} + +// Validate performs validation and exits with a nonzero status code if validation fails. +// The proper errors are printed to stderr. +func Validate(v Validator) { + errs := v.Validate() + + err := combineErrors(errs...) + if err != nil { + flog.Fatal("failed to validate this command\n%v", err) + } +}
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: