From 91288e1e5acd59135bcec1eb0d51ec11597f5d7d Mon Sep 17 00:00:00 2001 From: Derek Smith Date: Thu, 17 Jun 2021 13:43:37 -0500 Subject: [PATCH] feat(sm): add initial actions for AWS secrets manager Signed-off-by: Derek Smith --- .gitignore | 16 ++ aws/sm.go | 304 ++++++++++++++++++++++++++++++ cmd/logger.go | 20 ++ cmd/logger_test.go | 61 ++++++ cmd/sm.go | 461 +++++++++++++++++++++++++++++++++++++++++++++ docs/.keep | 0 go.mod | 17 ++ go.sum | 104 ++++++++++ info/app.go | 7 + info/help.go | 3 + main.go | 241 ++++++++++++++++++++++++ 11 files changed, 1234 insertions(+) create mode 100644 aws/sm.go create mode 100644 cmd/logger.go create mode 100644 cmd/logger_test.go create mode 100644 cmd/sm.go create mode 100644 docs/.keep create mode 100644 go.mod create mode 100644 go.sum create mode 100644 info/app.go create mode 100644 info/help.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore index 66fd13c..6e666d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +# Created by .ignore support plugin (hsz.mobi) +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen +### Go template # Binaries for programs and plugins *.exe *.exe~ @@ -13,3 +23,9 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +build/ +.DS_Store +bin/ +dist/ +sm \ No newline at end of file diff --git a/aws/sm.go b/aws/sm.go new file mode 100644 index 0000000..61bbb86 --- /dev/null +++ b/aws/sm.go @@ -0,0 +1,304 @@ +package sm + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/secretsmanager" + as "github.com/clok/awssession" +) + +func getValueByKey(keyName string, secretBytes []byte) (secret []byte, err error) { + var secrets map[string]interface{} + var secretValue string + + if err := json.Unmarshal(secretBytes, &secrets); err != nil { + return nil, err + } + + secretValue = fmt.Sprint(secrets[keyName]) + + return []byte(secretValue), nil +} + +// RetrieveSecret will pull the AWS Secrets Manager value and parse out the specific value needed. +// +// NOTE: Refactor of https://github.com/cyberark/summon-aws-secrets/blob/master/main.go +// This was needed to have the command return the byte stream rather than have it write +// to STDOUT +func RetrieveSecret(variableName string) (secretBytes []byte, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + // Check if key has been specified + arguments := strings.SplitN(variableName, "#", 2) + + secretName := arguments[0] + var keyName string + + if len(arguments) > 1 { + keyName = arguments[1] + } + + exists, err := CheckIfSecretExists(secretName) + if err != nil { + return nil, err + } + if !exists { + return nil, fmt.Errorf("'%s' secret does not exist", secretName) + } + + // Get secret value + req, resp := svc.GetSecretValueRequest(&secretsmanager.GetSecretValueInput{ + SecretId: aws.String(secretName), + }) + + err = req.Send() + if err != nil { + return nil, err + } + + if resp.SecretString != nil { + secretBytes = []byte(*resp.SecretString) + } else { + secretBytes = resp.SecretBinary + } + + if keyName != "" { + secretBytes, err = getValueByKey(keyName, secretBytes) + if err != nil { + return nil, err + } + } + + return +} + +// ListSecrets will retrieval ALL secrets via pagination of 100 per page. It will +// return once all pages have been processed. +func ListSecrets() (secrets []secretsmanager.SecretListEntry, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + // Get all secret names + err = svc.ListSecretsPages(&secretsmanager.ListSecretsInput{ + MaxResults: aws.Int64(100), + }, + func(page *secretsmanager.ListSecretsOutput, lastPage bool) bool { + for _, v := range page.SecretList { + secrets = append(secrets, *v) + } + return !lastPage + }) + if err != nil { + return nil, err + } + + return +} + +// GetSecret will retrieve a specific secret by Name (id) +func GetSecret(id string) (secret *secretsmanager.GetSecretValueOutput, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + secret, err = svc.GetSecretValue(&secretsmanager.GetSecretValueInput{ + SecretId: aws.String(id), + }) + + if err != nil { + return nil, err + } + + return +} + +// DeleteSecret will retrieve a specific secret by Name (id) +func DeleteSecret(id string, force bool) (secret *secretsmanager.DeleteSecretOutput, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + secret, err = svc.DeleteSecret(&secretsmanager.DeleteSecretInput{ + SecretId: aws.String(id), + ForceDeleteWithoutRecovery: aws.Bool(force), + }) + + if err != nil { + return nil, err + } + + return +} + +// PutSecretString will put an updated SecretString value to a specific secret by Name (id) +func PutSecretString(id string, data string) (secret *secretsmanager.PutSecretValueOutput, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + secret, err = svc.PutSecretValue(&secretsmanager.PutSecretValueInput{ + SecretString: aws.String(data), + SecretId: aws.String(id), + }) + + if err != nil { + return nil, err + } + + return +} + +// PutSecretBinary will put an updated SecretBinary value to a specific secret by Name (id) +func PutSecretBinary(id string, data []byte) (secret *secretsmanager.PutSecretValueOutput, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + secret, err = svc.PutSecretValue(&secretsmanager.PutSecretValueInput{ + SecretBinary: data, + SecretId: aws.String(id), + }) + + if err != nil { + return nil, err + } + + return +} + +// CreateSecretString will create a new SecretString value to a specific secret by Name (id) +func CreateSecretString(id string, data string, description string, tagsCSV string) (secret *secretsmanager.CreateSecretOutput, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + input := secretsmanager.CreateSecretInput{ + SecretString: aws.String(data), + Name: aws.String(id), + } + + if description != "" { + input.Description = aws.String(description) + } + + if tagsCSV != "" { + var tags []*secretsmanager.Tag + for _, kv := range strings.Split(tagsCSV, ",") { + parts := strings.SplitN(kv, "=", 2) + tags = append(tags, &secretsmanager.Tag{ + Key: aws.String(parts[0]), + Value: aws.String(parts[1]), + }) + } + input.Tags = tags + } + + secret, err = svc.CreateSecret(&input) + + if err != nil { + return nil, err + } + + return +} + +// CreateSecretBinary will create a new SecretBinary value to a specific secret by Name (id) +func CreateSecretBinary(id string, data []byte, description string, tagsCSV string) (secret *secretsmanager.CreateSecretOutput, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + input := secretsmanager.CreateSecretInput{ + SecretBinary: data, + Name: aws.String(id), + } + + if description != "" { + input.Description = aws.String(description) + } + + if tagsCSV != "" { + var tags []*secretsmanager.Tag + for _, kv := range strings.Split(tagsCSV, ",") { + parts := strings.SplitN(kv, "=", 2) + tags = append(tags, &secretsmanager.Tag{ + Key: aws.String(parts[0]), + Value: aws.String(parts[1]), + }) + } + input.Tags = tags + } + + secret, err = svc.CreateSecret(&input) + + if err != nil { + return nil, err + } + + return +} + +// DescribeSecret retrieves the describe data for a specific secret by Name (id) +func DescribeSecret(id string) (secret *secretsmanager.DescribeSecretOutput, err error) { + sess, err := as.New() + if err != nil { + return nil, err + } + svc := secretsmanager.New(sess) + + secret, err = svc.DescribeSecret(&secretsmanager.DescribeSecretInput{ + SecretId: aws.String(id), + }) + if err != nil { + return nil, err + } + + return +} + +// CheckIfSecretExists determines if the input secret ID already exists in AWS Secrets Manager +func CheckIfSecretExists(id string) (bool, error) { + sess, err := as.New() + if err != nil { + return true, err + } + svc := secretsmanager.New(sess) + + _, err = svc.DescribeSecret(&secretsmanager.DescribeSecretInput{ + SecretId: aws.String(id), + }) + + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + if aerr.Code() == secretsmanager.ErrCodeResourceNotFoundException { + return false, nil + } + } + return true, err + } + + return true, nil +} diff --git a/cmd/logger.go b/cmd/logger.go new file mode 100644 index 0000000..0f53161 --- /dev/null +++ b/cmd/logger.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "fmt" + "os" + + a "github.com/logrusorgru/aurora/v3" +) + +func PrintWarn(s string) { + _, _ = fmt.Fprintln(os.Stderr, a.Sprintf(a.Red("✖ %s"), s)) +} + +func PrintSuccess(s string) { + _, _ = fmt.Fprintln(os.Stderr, a.Sprintf(a.Green("✔ %s"), s)) +} + +func PrintInfo(s string) { + _, _ = fmt.Fprintln(os.Stderr, a.Sprintf(a.Gray(14, "➜ %s"), s)) +} diff --git a/cmd/logger_test.go b/cmd/logger_test.go new file mode 100644 index 0000000..d975b4c --- /dev/null +++ b/cmd/logger_test.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/logrusorgru/aurora/v3" +) + +func Test_PrintWarn(t *testing.T) { + rescueStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + PrintWarn("This is a test!") + + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stderr = rescueStderr + + wantMsg := fmt.Sprintln(aurora.Red("✖ This is a test!")) + if string(out) != wantMsg { + t.Errorf("%#v, wanted %#v", string(out), wantMsg) + } +} + +func Test_PrintSuccess(t *testing.T) { + rescueStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + PrintSuccess("This is a test!") + + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stderr = rescueStderr + + wantMsg := fmt.Sprintln(aurora.Green("✔ This is a test!")) + if string(out) != wantMsg { + t.Errorf("%#v, wanted %#v", string(out), wantMsg) + } +} + +func Test_PrintInfo(t *testing.T) { + rescueStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + PrintInfo("This is a test!") + + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stderr = rescueStderr + + wantMsg := fmt.Sprintln(aurora.Gray(14, "➜ This is a test!")) + if string(out) != wantMsg { + t.Errorf("%#v, wanted %#v", string(out), wantMsg) + } +} diff --git a/cmd/sm.go b/cmd/sm.go new file mode 100644 index 0000000..03f8a4d --- /dev/null +++ b/cmd/sm.go @@ -0,0 +1,461 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "sort" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/TylerBrock/colorjson" + "github.com/a8m/djson" + "github.com/aws/aws-sdk-go/aws" + "github.com/clok/sm/aws" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/urfave/cli/v2" +) + +// truncateString limits the length of a string while also appending an ellipses. +func truncateString(str string, num int) string { + short := str + if len(str) > num { + if num > 3 { + num -= 3 + } + short = str[0:num] + "..." + } + return short +} + +// selectSecretNameFromList is a helper method to either bypass and return the +// `secretName` passed in via CLI flag OR retrieve a list of all secrets to allow +// for a search select by the User. +func selectSecretNameFromList(c *cli.Context) (string, error) { + secretName := c.String("secret-id") + if secretName == "" { + secrets, err := sm.ListSecrets() + if err != nil { + PrintWarn("Error retrieving list of secrets.") + return "", err + } + + secretNames := make([]string, 0, len(secrets)) + for _, secret := range secrets { + secretNames = append(secretNames, aws.StringValue(secret.Name)) + } + sort.Strings(secretNames) + + p := &survey.Select{ + Message: "Choose a Secret to view:", + Options: secretNames, + Default: secretNames[0], + } + err = survey.AskOne(p, &secretName) + if err != nil { + return "", err + } + + PrintInfo(fmt.Sprintf("Retrieving: %s", secretName)) + } + return secretName, nil +} + +// promptForEdit is a helper method providing an editor interface. +func promptForEdit(secretName string, s []byte) ([]byte, error) { + ed := "" + prompt := &survey.Editor{ + Message: fmt.Sprintf("Open editor to modify '%s'?", secretName), + FileName: "*.json", + Default: string(s), + HideDefault: true, + AppendDefault: true, + } + err := survey.AskOne(prompt, &ed, nil) + if err != nil { + return nil, err + } + + return []byte(ed), nil +} + +// validateAndUpdateSecretValue will take the original buffer and interactively open an +// editor to allow for updates. It will check if the original and updated buffer match, if +// so it will exit gracefully. It will also verify valid JSON and prompt if an invalid input +// is provided. +func validateAndUpdateSecretValue(secretName string, orig []byte, updateTmp []byte) ([]byte, error) { + done := false + for !done { + _, err := djson.Decode(updateTmp) + if err != nil { + PrintWarn("invalid JSON submitted.") + + ed := false + p1 := &survey.Confirm{ + Message: "Open to edit?", + } + err = survey.AskOne(p1, &ed) + if err != nil { + return nil, err + } + if ed { + updateTmp, err = promptForEdit(secretName, updateTmp) + if err != nil { + return nil, cli.Exit(err, 2) + } + if string(orig) == strings.TrimSuffix(string(updateTmp), "\n") { + PrintInfo("Updated value matches original. Exiting.") + return nil, cli.Exit("", 0) + } + } else { + submit := false + p2 := &survey.Confirm{ + Message: "Continue with Submit?", + } + err = survey.AskOne(p2, &submit) + if err != nil { + return nil, err + } + if !submit { + PrintWarn("Exiting without submit.") + return nil, cli.Exit("", 0) + } + PrintInfo("Continuing with submit.") + done = true + } + } else { + PrintInfo("JSON validated.") + done = true + } + } + return updateTmp, nil +} + +// ListSecrets CLI command to list all Secrets. +func ListSecrets(c *cli.Context) error { + secrets, err := sm.ListSecrets() + if err != nil { + return cli.Exit(err, 2) + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + t.AppendHeader(table.Row{"Name", "Updated", "Accessed", "Description"}) + t.SetColumnConfigs([]table.ColumnConfig{ + {Name: "Name", WidthMax: 120}, + {Name: "Updated", WidthMax: 10}, + {Name: "Accessed", WidthMax: 10}, + { + Name: "Description", + WidthMax: 40, + }, + }) + t.SortBy([]table.SortBy{ + {Name: "Name", Mode: table.Asc}, + }) + + for _, secret := range secrets { + lastdt := aws.TimeValue(secret.LastAccessedDate) + updateddt := aws.TimeValue(secret.LastChangedDate) + t.AppendRow([]interface{}{ + aws.StringValue(secret.Name), + fmt.Sprintf("%d-%02d-%02d", updateddt.Year(), updateddt.Month(), updateddt.Day()), + fmt.Sprintf("%d-%02d-%02d", lastdt.Year(), lastdt.Month(), lastdt.Day()), + truncateString(aws.StringValue(secret.Description), 40), + }) + } + + t.Render() + + return nil +} + +// ViewSecret CLI command to view/get a Secret. +func ViewSecret(c *cli.Context) error { + secretName, err := selectSecretNameFromList(c) + if err != nil { + return cli.Exit(err, 2) + } + + secret, err := sm.GetSecret(secretName) + if err != nil { + return cli.Exit(err, 2) + } + + if c.Bool("binary") { + fmt.Println(string(secret.SecretBinary)) + } else { + result, err := djson.Decode([]byte(aws.StringValue(secret.SecretString))) + if err != nil { + PrintWarn("stored string value is not valid JSON.") + fmt.Println(aws.StringValue(secret.SecretString)) + } else { + f := colorjson.NewFormatter() + f.Indent = 4 + + s, _ := f.Marshal(result) + fmt.Println(string(s)) + } + } + + return nil +} + +// DescribeSecret CLI command to describe a Secret. +func DescribeSecret(c *cli.Context) error { + secretName, err := selectSecretNameFromList(c) + if err != nil { + return cli.Exit(err, 2) + } + + secret, err := sm.DescribeSecret(secretName) + if err != nil { + return cli.Exit(err, 2) + } + + fmt.Println(secret.String()) + + return nil +} + +// EditSecret CLI command to edit a Secret. +func EditSecret(c *cli.Context) error { + secretName, err := selectSecretNameFromList(c) + if err != nil { + return cli.Exit(err, 2) + } + + secret, err := sm.GetSecret(secretName) + if err != nil { + return cli.Exit(err, 2) + } + + var s []byte + if c.Bool("binary") { + s = secret.SecretBinary + } else { + result, err := djson.Decode([]byte(aws.StringValue(secret.SecretString))) + if err != nil { + PrintWarn("stored string value is not valid JSON.") + s = []byte(aws.StringValue(secret.SecretString)) + } else { + s, err = json.MarshalIndent(result, "", " ") + if err != nil { + return cli.Exit(err, 2) + } + } + } + + var up []byte + up, err = promptForEdit(secretName, s) + if err != nil { + return cli.Exit(err, 2) + } + if string(s) == strings.TrimSuffix(string(up), "\n") { + PrintInfo("Updated value matches original. Exiting.") + return nil + } + + var final []byte + final, err = validateAndUpdateSecretValue(secretName, s, up) + if err != nil { + return cli.Exit(err, 2) + } + + var t string + if c.Bool("binary") { + t = "BinarySecret" + _, err = sm.PutSecretBinary(secretName, final) + } else { + t = "StringSecret" + _, err = sm.PutSecretString(secretName, string(final)) + } + + if err != nil { + return cli.Exit(err, 2) + } + + PrintSuccess(fmt.Sprintf("%s %s successfully updated.", secretName, t)) + + return nil +} + +// CreateSecret CLI command to create a new Secret. +func CreateSecret(c *cli.Context) error { + secretName := c.String("secret-id") + exists, err := sm.CheckIfSecretExists(secretName) + if err != nil { + return cli.Exit(err, 2) + } + if exists { + PrintWarn(fmt.Sprintf("'%s' already exists. Please use a different name.", secretName)) + return nil + } + + interactive := c.Bool("interactive") + var value []byte + if c.String("value") == "" { + // Assume interactive mode + interactive = true + value = []byte("{}") + } else { + value = []byte(c.String("value")) + } + + var s []byte + if interactive { + result, err := djson.Decode(value) + if err != nil { + PrintWarn("value is not valid JSON.") + s = value + } else { + s, err = json.MarshalIndent(result, "", " ") + if err != nil { + return cli.Exit(err, 2) + } + } + + var up []byte + up, err = promptForEdit(secretName, s) + if err != nil { + return cli.Exit(err, 2) + } + s = up + } else { + s = value + } + + var t string + if c.Bool("binary") { + t = "BinarySecret" + _, err = sm.CreateSecretBinary(secretName, s, c.String("description"), c.String("tags")) + } else { + t = "StringSecret" + _, err = sm.CreateSecretString(secretName, string(s), c.String("description"), c.String("tags")) + } + + if err != nil { + return cli.Exit(err, 2) + } + + PrintSuccess(fmt.Sprintf("%s %s successfully created.", secretName, t)) + + return nil +} + +// PutSecret CLI command to apply a delta to a Secret. +func PutSecret(c *cli.Context) error { + secretName := c.String("secret-id") + exists, err := sm.CheckIfSecretExists(secretName) + if err != nil { + return cli.Exit(err, 2) + } + if !exists { + PrintWarn(fmt.Sprintf("'%s' does not exists. Please create the secret first.", secretName)) + return nil + } + + interactive := c.Bool("interactive") + var value []byte + if c.String("value") == "" { + // Assume interactive mode + interactive = true + + secret, err := sm.GetSecret(secretName) + if err != nil { + return cli.Exit(err, 2) + } + + if c.Bool("binary") { + value = secret.SecretBinary + } else { + result, err := djson.Decode([]byte(aws.StringValue(secret.SecretString))) + if err != nil { + PrintWarn("stored string value is not valid JSON.") + value = []byte(aws.StringValue(secret.SecretString)) + } else { + value, err = json.MarshalIndent(result, "", " ") + if err != nil { + return cli.Exit(err, 2) + } + } + } + } else { + value = []byte(c.String("value")) + } + + var final []byte + if interactive { + var up []byte + up, err = promptForEdit(secretName, value) + if err != nil { + return cli.Exit(err, 2) + } + if string(value) == strings.TrimSuffix(string(up), "\n") { + PrintInfo("Updated value matches original. Exiting.") + return nil + } + + final, err = validateAndUpdateSecretValue(secretName, value, up) + if err != nil { + return cli.Exit(err, 2) + } + } else { + final = value + } + + var t string + if c.Bool("binary") { + t = "BinarySecret" + _, err = sm.PutSecretBinary(secretName, final) + } else { + t = "StringSecret" + _, err = sm.PutSecretString(secretName, string(final)) + } + + if err != nil { + return cli.Exit(err, 2) + } + + PrintSuccess(fmt.Sprintf("%s %s successfully put new version.", secretName, t)) + + return nil +} + +// DeleteSecret CLI command that will delete a Secret. +func DeleteSecret(c *cli.Context) error { + secretName := c.String("secret-id") + exists, err := sm.CheckIfSecretExists(secretName) + if err != nil { + return cli.Exit(err, 2) + } + if !exists { + PrintWarn(fmt.Sprintf("'%s' was not found.", secretName)) + return nil + } + + del := false + p1 := &survey.Confirm{ + Message: fmt.Sprintf("Are you sure you want to permanentaly delete '%s'?", secretName), + } + err = survey.AskOne(p1, &del) + if err != nil { + return cli.Exit(err, 2) + } + + if !del { + PrintInfo("Exiting without delete.") + return nil + } + + force := c.Bool("force") + _, err = sm.DeleteSecret(secretName, force) + if err != nil { + return cli.Exit(err, 2) + } + + PrintSuccess(fmt.Sprintf("'%s' deleted. (force: %v)", secretName, force)) + + return nil +} diff --git a/docs/.keep b/docs/.keep new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c4514f3 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/clok/sm + +go 1.16 + +require ( + github.com/AlecAivazis/survey/v2 v2.2.12 + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 + github.com/a8m/djson v0.0.0-20170509170705-c02c5aef757f + github.com/aws/aws-sdk-go v1.38.63 + github.com/clok/awssession v1.1.2 + github.com/clok/cdocs v1.2.0 + github.com/fatih/color v1.12.0 // indirect + github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519 // indirect + github.com/jedib0t/go-pretty/v6 v6.2.2 + github.com/logrusorgru/aurora/v3 v3.0.0 + github.com/urfave/cli/v2 v2.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9adc706 --- /dev/null +++ b/go.sum @@ -0,0 +1,104 @@ +github.com/AlecAivazis/survey/v2 v2.2.12 h1:5a07y93zA6SZ09gOa9wLVLznF5zTJMQ+pJ3cZK4IuO8= +github.com/AlecAivazis/survey/v2 v2.2.12/go.mod h1:6d4saEvBsfSHXeN1a5OA5m2+HJ2LuVokllnC77pAIKI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= +github.com/a8m/djson v0.0.0-20170509170705-c02c5aef757f h1:su5fhWd5UCmmRQEFPQPalJ304Qtcgk9ZDDnKnvpsraU= +github.com/a8m/djson v0.0.0-20170509170705-c02c5aef757f/go.mod h1:w3s8fnedJo6LJQ7dUUf1OcetqgS1hGpIDjY5bBowg1Y= +github.com/aws/aws-sdk-go v1.38.63 h1:BqPxe0sujTRTbir6OWj0f1VmeJcAIv7ZhTCAhaU1zmE= +github.com/aws/aws-sdk-go v1.38.63/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/clok/awssession v1.1.2 h1:ZSAsQu3dAUSmySRJTpvCogwY01ry+43qmEPoM3FL/rU= +github.com/clok/awssession v1.1.2/go.mod h1:5g1RJIhuBYhNLoX6OLeJH53GQWLChj8Piv2GDDWLa3g= +github.com/clok/cdocs v1.2.0 h1:sFhbBmTgV1rC8TjRSzBD7RIXP4fSnw/gTBPd5YuRBJM= +github.com/clok/cdocs v1.2.0/go.mod h1:MHdLYRxv66KNophPTW4VJOkdGJhDhpSJ1ka6WuIbrJg= +github.com/clok/kemba v1.1.1 h1:s1G0dd7NrScBuYcBHQNzPJ1u3q/herMuY9VGYFSleTA= +github.com/clok/kemba v1.1.1/go.mod h1:rsYZSVvRq+nopEvJpMRWDV+pbg4pUKMtXjAuTLHAENQ= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= +github.com/gookit/color v1.3.8 h1:w2WcSwaCa1ojRWO60Mm4GJUJomBNKR9G+x9DwaaCL1c= +github.com/gookit/color v1.3.8/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= +github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519 h1:nqAlWFEdqI0ClbTDrhDvE/8LeQ4pftrqKUX9w5k0j3s= +github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= +github.com/jedib0t/go-pretty/v6 v6.2.2 h1:o3McN0rQ4X+IU+HduppSp9TwRdGLRW2rhJXy9CJaCRw= +github.com/jedib0t/go-pretty/v6 v6.2.2/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= +github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= +github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/info/app.go b/info/app.go new file mode 100644 index 0000000..8f2cc07 --- /dev/null +++ b/info/app.go @@ -0,0 +1,7 @@ +package info + +// AppRepoOwner defined the owner of the repo on GitHub +const AppRepoOwner string = "Derek Smith " + +// AppName defined the application name +const AppName string = "sm" diff --git a/info/help.go b/info/help.go new file mode 100644 index 0000000..0b1d5fe --- /dev/null +++ b/info/help.go @@ -0,0 +1,3 @@ +package info + +var SecretBinaryHelp = "By default, the tool will interact with the SecretString value. Use this flag to interact with the SecretBinary value instead." diff --git a/main.go b/main.go new file mode 100644 index 0000000..5701764 --- /dev/null +++ b/main.go @@ -0,0 +1,241 @@ +package main + +import ( + "fmt" + "log" + "os" + "runtime" + "time" + + "github.com/clok/cdocs" + "github.com/clok/sm/cmd" + "github.com/clok/sm/info" + "github.com/urfave/cli/v2" +) + +var version string + +func main() { + // Generate the install-manpage command + im, err := cdocs.InstallManpageCommand(&cdocs.InstallManpageCommandInput{ + AppName: info.AppName, + Hidden: true, + }) + if err != nil { + log.Fatal(err) + } + + app := &cli.App{ + Name: info.AppName, + Version: version, + Compiled: time.Now(), + Authors: []*cli.Author{ + { + Name: "Derek Smith", + Email: "dsmith@goodwaygroup.com", + }, + { + Name: info.AppRepoOwner, + }, + }, + Copyright: "(c) 2021 Derek Smith", + HelpName: info.AppName, + Usage: "interact with config map and secret manager variables", + EnableBashCompletion: true, + Commands: []*cli.Command{ + { + // list-secrets + Name: "list", + Usage: "display table of all secrets with meta data", + Action: cmd.ListSecrets, + }, + { + // describe-secret + Name: "describe", + Usage: "print description of secret to `STDOUT`", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "secret-id", + Aliases: []string{"s"}, + Usage: "Specific Secret to describe, will bypass select/search", + }, + }, + Action: cmd.DescribeSecret, + }, + { + // get-secret-value + Name: "get", + Aliases: []string{"view"}, + Usage: "select from list or pass in specific secret", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "secret-id", + Aliases: []string{"s"}, + Usage: "Specific Secret to view, will bypass select/search", + }, + &cli.BoolFlag{ + Name: "binary", + Aliases: []string{"b"}, + Usage: "get the SecretBinary value", + DefaultText: info.SecretBinaryHelp, + }, + }, + Action: cmd.ViewSecret, + }, + { + Name: "edit", + Aliases: []string{"e"}, + Usage: "interactive edit of a secret String Value", + // TODO: add UsageText + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "secret-id", + Aliases: []string{"s"}, + Usage: "Specific Secret to edit, will bypass select/search", + }, + &cli.BoolFlag{ + Name: "binary", + Aliases: []string{"b"}, + Usage: "get the SecretBinary value", + DefaultText: info.SecretBinaryHelp, + }, + // TODO: add flag for passing version stage + }, + Action: cmd.EditSecret, + }, + { + // create-secret + Name: "create", + Aliases: []string{"c"}, + Usage: "create new secret in Secrets Manager", + // TODO: add UsageText + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "secret-id", + Aliases: []string{"s"}, + Usage: "Secret name", + Required: true, + }, + &cli.BoolFlag{ + Name: "binary", + Aliases: []string{"b"}, + Usage: "get the SecretBinary value", + DefaultText: info.SecretBinaryHelp, + }, + &cli.StringFlag{ + Name: "value", + Aliases: []string{"v"}, + Usage: "Secret Value. Will store as a string, unless binary flag is set.", + }, + &cli.BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Usage: "Open interactive editor to create secret value. If no 'value' is provided, an editor will be opened by default.", + }, + &cli.StringFlag{ + Name: "description", + Aliases: []string{"d"}, + Usage: "Additional description text.", + }, + &cli.StringFlag{ + Name: "tags", + Aliases: []string{"t"}, + Usage: "key=value tags (CSV list)", + }, + }, + Action: cmd.CreateSecret, + }, + { + // put-secret-value + Name: "put", + Usage: "non-interactive update to a specific secret", + UsageText: ` +Stores a new encrypted secret value in the specified secret. To do this, the +operation creates a new version and attaches it to the secret. The version +can contain a new SecretString value or a new SecretBinary value. + +This will put the value to AWSCURRENT and retain one previous version +with AWSPREVIOUS. +`, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "secret-id", + Aliases: []string{"s"}, + Usage: "Secret name", + Required: true, + }, + &cli.BoolFlag{ + Name: "binary", + Aliases: []string{"b"}, + Usage: "get the SecretBinary value", + DefaultText: info.SecretBinaryHelp, + }, + &cli.StringFlag{ + Name: "value", + Aliases: []string{"v"}, + Usage: "Secret Value. Will store as a string, unless binary flag is set.", + }, + &cli.BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Usage: "Override and open interactive editor to verify and modify the new secret value.", + }, + // TODO: add flag for passing version stage + }, + Action: cmd.PutSecret, + }, + { + Name: "delete", + Aliases: []string{"del"}, + Usage: "delete a specific secret", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "secret-id", + Aliases: []string{"s"}, + Usage: "Specific Secret to delete", + Required: true, + }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "Bypass recovery window (30 days) and immediately delete Secret.", + }, + }, + Action: cmd.DeleteSecret, + }, + im, + { + Name: "version", + Aliases: []string{"v"}, + Usage: "Print version info", + Action: func(c *cli.Context) error { + fmt.Printf("%s %s (%s/%s)\n", info.AppName, version, runtime.GOOS, runtime.GOARCH) + return nil + }, + }, + }, + } + + if os.Getenv("DOCS_MD") != "" { + docs, err := cdocs.ToMarkdown(app) + if err != nil { + panic(err) + } + fmt.Println(docs) + return + } + + if os.Getenv("DOCS_MAN") != "" { + docs, err := cdocs.ToMan(app) + if err != nil { + panic(err) + } + fmt.Println(docs) + return + } + + err = app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +}