All checks were successful
continuous-integration/drone/push Build is passing
See #627
654 lines
16 KiB
Go
654 lines
16 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
appPkg "coopcloud.tech/abra/pkg/app"
|
|
"coopcloud.tech/abra/pkg/autocomplete"
|
|
"coopcloud.tech/abra/pkg/client"
|
|
"coopcloud.tech/abra/pkg/formatter"
|
|
"coopcloud.tech/abra/pkg/i18n"
|
|
"coopcloud.tech/abra/pkg/log"
|
|
"coopcloud.tech/abra/pkg/secret"
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/docker/docker/api/types"
|
|
dockerClient "github.com/docker/docker/client"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// translators: `abra app secret generate` aliases. use a comma separated list of aliases with
|
|
// no spaces in between
|
|
var appSecretGenerateAliases = i18n.G("g")
|
|
|
|
var AppSecretGenerateCommand = &cobra.Command{
|
|
// translators: `app secret generate` command
|
|
Use: i18n.G("generate <domain> [[secret] [version] | --all] [flags]"),
|
|
Aliases: strings.Split(appSecretGenerateAliases, ","),
|
|
// translators: Short description for `app secret generate` command
|
|
Short: i18n.G("Generate secrets"),
|
|
Args: cobra.RangeArgs(1, 3),
|
|
ValidArgsFunction: func(
|
|
cmd *cobra.Command,
|
|
args []string,
|
|
toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
switch l := len(args); l {
|
|
case 0:
|
|
return autocomplete.AppNameComplete()
|
|
case 1:
|
|
app, err := appPkg.Get(args[0])
|
|
if err != nil {
|
|
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
|
|
}
|
|
return autocomplete.SecretComplete(app.Recipe.Name)
|
|
default:
|
|
return nil, cobra.ShellCompDirectiveDefault
|
|
}
|
|
},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
app := internal.ValidateApp(args)
|
|
|
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if len(args) <= 2 && !generateAllSecrets {
|
|
log.Fatal(i18n.G("missing arguments [secret]/[version] or '--all'"))
|
|
}
|
|
|
|
if len(args) > 2 && generateAllSecrets {
|
|
log.Fatal(i18n.G("cannot use '[secret] [version]' and '--all' together"))
|
|
}
|
|
|
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if !generateAllSecrets {
|
|
secretName := args[1]
|
|
secretVersion := args[2]
|
|
s, ok := secrets[secretName]
|
|
if !ok {
|
|
log.Fatal(i18n.G("%s doesn't exist in the env config?", secretName))
|
|
}
|
|
s.Version = secretVersion
|
|
secrets = map[string]secret.Secret{
|
|
secretName: s,
|
|
}
|
|
}
|
|
|
|
cl, err := client.New(app.Server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if storeInPass {
|
|
for name, data := range secretVals {
|
|
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(secretVals) == 0 {
|
|
log.Warn(i18n.G("no secrets generated"))
|
|
os.Exit(1)
|
|
}
|
|
|
|
headers := []string{i18n.G("NAME"), i18n.G("VALUE")}
|
|
table, err := formatter.CreateTable()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
table.Headers(headers...)
|
|
|
|
var rows [][]string
|
|
for name, val := range secretVals {
|
|
row := []string{name, val}
|
|
rows = append(rows, row)
|
|
table.Row(row...)
|
|
}
|
|
|
|
if internal.MachineReadable {
|
|
out, err := formatter.ToJSON(headers, rows)
|
|
if err != nil {
|
|
log.Fatal(i18n.G("unable to render to JSON: %s", err))
|
|
}
|
|
fmt.Println(out)
|
|
return
|
|
}
|
|
|
|
if err := formatter.PrintTable(table); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
log.Warn(i18n.G(
|
|
"generated secrets %s shown again, please take note of them %s",
|
|
formatter.BoldStyle.Render(i18n.G("NOT")),
|
|
formatter.BoldStyle.Render(i18n.G("NOW")),
|
|
))
|
|
},
|
|
}
|
|
|
|
// translators: `abra app secret insert` aliases. use a comma separated list of aliases with
|
|
// no spaces in between
|
|
var appSecretInsertAliases = i18n.G("i")
|
|
|
|
var AppSecretInsertCommand = &cobra.Command{
|
|
// translators: `app secret insert` command
|
|
Use: i18n.G("insert <domain> <secret> <version> [<data>] [flags]"),
|
|
Aliases: strings.Split(appSecretInsertAliases, ","),
|
|
// translators: Short description for `app secret insert` command
|
|
Short: i18n.G("Insert secret"),
|
|
Long: i18n.G(`This command inserts a secret into an app environment.
|
|
|
|
Arbitrary secret insertion is not supported. Secrets that are inserted must
|
|
match those configured in the recipe beforehand.
|
|
|
|
This can be useful when you want to manually generate secrets for an app
|
|
environment. Typically, you can let Abra generate them for you on app creation
|
|
(see "abra app new --secrets/-S" for more).`),
|
|
Example: i18n.G(` # insert regular secret
|
|
abra app secret insert 1312.net my_secret v1 mySuperSecret
|
|
|
|
# insert secret as file
|
|
abra app secret insert 1312.net my_secret v1 secret.txt -f
|
|
|
|
# insert secret from stdin
|
|
echo "mmySuperSecret" | abra app secret insert 1312.net my_secret v1`),
|
|
Args: cobra.MinimumNArgs(3),
|
|
ValidArgsFunction: func(
|
|
cmd *cobra.Command,
|
|
args []string,
|
|
toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
switch l := len(args); l {
|
|
case 0:
|
|
return autocomplete.AppNameComplete()
|
|
case 1:
|
|
app, err := appPkg.Get(args[0])
|
|
if err != nil {
|
|
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
|
|
}
|
|
return autocomplete.SecretComplete(app.Recipe.Name)
|
|
default:
|
|
return nil, cobra.ShellCompDirectiveDefault
|
|
}
|
|
},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
app := internal.ValidateApp(args)
|
|
|
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
cl, err := client.New(app.Server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
name := args[1]
|
|
version := args[2]
|
|
data, err := readSecretData(args)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
var isRecipeSecret bool
|
|
for secretName := range secrets {
|
|
if secretName == name {
|
|
isRecipeSecret = true
|
|
}
|
|
}
|
|
if !isRecipeSecret {
|
|
log.Fatal(i18n.G("no secret %s available for recipe %s?", name, app.Recipe.Name))
|
|
}
|
|
|
|
if insertFromFile {
|
|
raw, err := os.ReadFile(data)
|
|
if err != nil {
|
|
log.Fatal(i18n.G("reading secret from file: %s", err))
|
|
}
|
|
data = string(raw)
|
|
}
|
|
|
|
if trimInput {
|
|
data = strings.TrimSpace(data)
|
|
}
|
|
|
|
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
|
|
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
log.Info(i18n.G("%s successfully stored on server", secretName))
|
|
|
|
if storeInPass {
|
|
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
func readSecretData(args []string) (string, error) {
|
|
if len(args) == 4 {
|
|
return args[3], nil
|
|
}
|
|
|
|
if len(args) != 3 {
|
|
return "", errors.New(i18n.G("need 3 or 4 arguments"))
|
|
}
|
|
// First check if data is provided by stdin
|
|
fi, err := os.Stdin.Stat()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if fi.Mode()&os.ModeNamedPipe != 0 {
|
|
// Can't insert from stdin and read from file
|
|
if insertFromFile {
|
|
return "", errors.New(i18n.G("can not insert from file and read from stdin"))
|
|
}
|
|
|
|
log.Debug(i18n.G("reading secret data from stdin"))
|
|
bytes, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return "", errors.New(i18n.G("reading data from stdin: %s", err))
|
|
}
|
|
|
|
return string(bytes), nil
|
|
}
|
|
if internal.NoInput {
|
|
return "", errors.New(i18n.G("must provide <data> argument if --no-input is passed"))
|
|
}
|
|
|
|
log.Debug(i18n.G("secret data not provided on command-line or stdin, prompting"))
|
|
var prompt survey.Prompt
|
|
if !insertFromFile {
|
|
prompt = &survey.Password{
|
|
Message: i18n.G("specify secret value"),
|
|
}
|
|
} else {
|
|
prompt = &survey.Input{
|
|
Message: i18n.G("specify secret file"),
|
|
}
|
|
}
|
|
var data string
|
|
if err := survey.AskOne(prompt, &data); err != nil {
|
|
return "", err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// secretRm removes a secret.
|
|
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error {
|
|
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Info(i18n.G("deleted %s successfully from server", secretName))
|
|
|
|
if removeFromPass {
|
|
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Info(i18n.G("deleted %s successfully from local pass store", secretName))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// translators: `abra app secret remove` aliases. use a comma separated list of aliases with
|
|
// no spaces in between
|
|
var appSecretRemoveAliases = i18n.G("rm")
|
|
|
|
var AppSecretRmCommand = &cobra.Command{
|
|
// translators: `app secret remove` command
|
|
Use: i18n.G("remove <domain> [[secret] | --all] [flags]"),
|
|
Aliases: strings.Split(appSecretRemoveAliases, ","),
|
|
// translators: Short description for `app secret remove` command
|
|
Short: i18n.G("Remove a secret"),
|
|
Long: i18n.G(`This command removes a secret from an app environment.
|
|
|
|
Arbitrary secret removal is not supported. Secrets that are removed must
|
|
match those configured in the recipe beforehand.`),
|
|
Example: i18n.G(" abra app secret rm 1312.net oauth_key"),
|
|
Args: cobra.RangeArgs(1, 2),
|
|
ValidArgsFunction: func(
|
|
cmd *cobra.Command,
|
|
args []string,
|
|
toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
switch l := len(args); l {
|
|
case 0:
|
|
return autocomplete.AppNameComplete()
|
|
case 1:
|
|
if !rmAllSecrets {
|
|
app, err := appPkg.Get(args[0])
|
|
if err != nil {
|
|
return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
|
|
}
|
|
return autocomplete.SecretComplete(app.Recipe.Name)
|
|
}
|
|
return nil, cobra.ShellCompDirectiveDefault
|
|
default:
|
|
return nil, cobra.ShellCompDirectiveError
|
|
}
|
|
},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
app := internal.ValidateApp(args)
|
|
|
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if len(args) == 2 && rmAllSecrets {
|
|
log.Fatal(i18n.G("cannot use [secret] and --all/-a together"))
|
|
}
|
|
|
|
if len(args) != 2 && !rmAllSecrets {
|
|
log.Fatal(i18n.G("no secret(s) specified?"))
|
|
}
|
|
|
|
cl, err := client.New(app.Server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
filters, err := app.Filters(false, false)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
remoteSecretNames := make(map[string]bool)
|
|
for _, cont := range secretList {
|
|
remoteSecretNames[cont.Spec.Annotations.Name] = true
|
|
}
|
|
|
|
var secretToRm string
|
|
if len(args) == 2 {
|
|
secretToRm = args[1]
|
|
}
|
|
|
|
match := false
|
|
for secretName, val := range secrets {
|
|
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
|
|
if _, ok := remoteSecretNames[secretRemoteName]; ok {
|
|
if secretToRm != "" {
|
|
if secretName == secretToRm {
|
|
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return
|
|
}
|
|
} else {
|
|
match = true
|
|
|
|
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !match && secretToRm != "" {
|
|
log.Fatal(i18n.G("%s doesn't exist on server?", secretToRm))
|
|
}
|
|
|
|
if !match {
|
|
log.Fatal(i18n.G("no secrets to remove?"))
|
|
}
|
|
},
|
|
}
|
|
|
|
// translators: `abra app secret ls` aliases. use a comma separated list of aliases with
|
|
// no spaces in between
|
|
var appSecretLsAliases = i18n.G("ls")
|
|
|
|
var AppSecretLsCommand = &cobra.Command{
|
|
// translators: `app secret list` command
|
|
Use: i18n.G("list <domain>"),
|
|
Aliases: strings.Split(appSecretLsAliases, ","),
|
|
// translators: Short description for `app secret list` command
|
|
Short: i18n.G("List all secrets"),
|
|
Args: cobra.MinimumNArgs(1),
|
|
ValidArgsFunction: func(
|
|
cmd *cobra.Command,
|
|
args []string,
|
|
toComplete string,
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
return autocomplete.AppNameComplete()
|
|
},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
app := internal.ValidateApp(args)
|
|
|
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
cl, err := client.New(app.Server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
headers := []string{i18n.G("NAME"), i18n.G("VERSION"), i18n.G("GENERATED NAME"), i18n.G("CREATED ON SERVER")}
|
|
table, err := formatter.CreateTable()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
table.Headers(headers...)
|
|
|
|
secStats, err := secret.PollSecretsStatus(cl, app)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Sort secrets to ensure reproducible output
|
|
sort.Slice(secStats, func(i, j int) bool {
|
|
return secStats[i].LocalName < secStats[j].LocalName
|
|
})
|
|
var rows [][]string
|
|
for _, secStat := range secStats {
|
|
row := []string{
|
|
secStat.LocalName,
|
|
secStat.Version,
|
|
secStat.RemoteName,
|
|
strconv.FormatBool(secStat.CreatedOnRemote),
|
|
}
|
|
|
|
rows = append(rows, row)
|
|
table.Row(row...)
|
|
}
|
|
|
|
if len(rows) > 0 {
|
|
if internal.MachineReadable {
|
|
out, err := formatter.ToJSON(headers, rows)
|
|
if err != nil {
|
|
log.Fatal(i18n.G("unable to render to JSON: %s", err))
|
|
}
|
|
fmt.Println(out)
|
|
return
|
|
}
|
|
|
|
if err := formatter.PrintTable(table); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
log.Warn(i18n.G("no secrets stored for %s", app.Name))
|
|
},
|
|
}
|
|
|
|
var AppSecretCommand = &cobra.Command{
|
|
// translators: `app secret` command group
|
|
Use: i18n.G("secret [cmd] [args] [flags]"),
|
|
Aliases: []string{i18n.G("s")},
|
|
// translators: Short description for `app secret` command group
|
|
Short: i18n.G("Manage app secrets"),
|
|
}
|
|
|
|
var (
|
|
storeInPass bool
|
|
insertFromFile bool
|
|
trimInput bool
|
|
rmAllSecrets bool
|
|
generateAllSecrets bool
|
|
removeFromPass bool
|
|
)
|
|
|
|
func init() {
|
|
AppSecretGenerateCommand.Flags().BoolVarP(
|
|
&internal.MachineReadable,
|
|
i18n.G("machine"),
|
|
i18n.G("m"),
|
|
false,
|
|
i18n.G("print machine-readable output"),
|
|
)
|
|
|
|
AppSecretGenerateCommand.Flags().BoolVarP(
|
|
&storeInPass,
|
|
i18n.G("pass"),
|
|
i18n.G("p"),
|
|
false,
|
|
i18n.G("store generated secrets in a local pass store"),
|
|
)
|
|
|
|
AppSecretGenerateCommand.Flags().BoolVarP(
|
|
&internal.Chaos,
|
|
i18n.G("chaos"),
|
|
i18n.G("C"),
|
|
false,
|
|
i18n.G("ignore uncommitted recipes changes"),
|
|
)
|
|
|
|
AppSecretGenerateCommand.Flags().BoolVarP(
|
|
&generateAllSecrets,
|
|
i18n.G("all"),
|
|
i18n.G("a"),
|
|
false,
|
|
i18n.G("generate all secrets"),
|
|
)
|
|
|
|
AppSecretInsertCommand.Flags().BoolVarP(
|
|
&storeInPass,
|
|
i18n.G("pass"),
|
|
i18n.G("p"),
|
|
false,
|
|
i18n.G("store generated secrets in a local pass store"),
|
|
)
|
|
|
|
AppSecretInsertCommand.Flags().BoolVarP(
|
|
&insertFromFile,
|
|
i18n.G("file"),
|
|
i18n.G("f"),
|
|
false,
|
|
i18n.G("treat input as a file"),
|
|
)
|
|
|
|
AppSecretInsertCommand.Flags().BoolVarP(
|
|
&trimInput,
|
|
i18n.G("trim"),
|
|
i18n.G("t"),
|
|
false,
|
|
i18n.G("trim input"),
|
|
)
|
|
|
|
AppSecretInsertCommand.Flags().BoolVarP(
|
|
&internal.Chaos,
|
|
i18n.G("chaos"),
|
|
i18n.G("C"),
|
|
false,
|
|
i18n.G("ignore uncommitted recipes changes"),
|
|
)
|
|
|
|
AppSecretRmCommand.Flags().BoolVarP(
|
|
&rmAllSecrets,
|
|
i18n.G("all"),
|
|
i18n.G("a"),
|
|
false,
|
|
i18n.G("remove all secrets"),
|
|
)
|
|
|
|
AppSecretRmCommand.Flags().BoolVarP(
|
|
&removeFromPass,
|
|
i18n.G("pass"),
|
|
i18n.G("p"),
|
|
false,
|
|
i18n.G("remove generated secrets from a local pass store"),
|
|
)
|
|
|
|
AppSecretRmCommand.Flags().BoolVarP(
|
|
&internal.Chaos,
|
|
i18n.G("chaos"),
|
|
i18n.G("C"),
|
|
false,
|
|
i18n.G("ignore uncommitted recipes changes"),
|
|
)
|
|
|
|
AppSecretLsCommand.Flags().BoolVarP(
|
|
&internal.Chaos,
|
|
i18n.G("chaos"),
|
|
i18n.G("C"),
|
|
false,
|
|
i18n.G("ignore uncommitted recipes changes"),
|
|
)
|
|
|
|
AppSecretLsCommand.Flags().BoolVarP(
|
|
&internal.MachineReadable,
|
|
i18n.G("machine"),
|
|
i18n.G("m"),
|
|
false,
|
|
i18n.G("print machine-readable output"),
|
|
)
|
|
}
|