package app import ( "context" "fmt" "os" "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/log" "coopcloud.tech/abra/pkg/secret" "github.com/docker/docker/api/types" dockerClient "github.com/docker/docker/client" "github.com/spf13/cobra" ) var AppSecretGenerateCommand = &cobra.Command{ Use: "generate [[secret] [version] | --all] [flags]", Aliases: []string{"g"}, Short: "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 { errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{errMsg}, 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) == 1 && !generateAllSecrets { log.Fatal("missing arguments [secret]/[version] or '--all'") } if len(args) > 1 && generateAllSecrets { log.Fatal("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.Fatalf("%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("no secrets generated") os.Exit(1) } headers := []string{"NAME", "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("unable to render to JSON: %s", err) } fmt.Println(out) return } if err := formatter.PrintTable(table); err != nil { log.Fatal(err) } log.Warnf( "generated secrets %s shown again, please take note of them %s", formatter.BoldStyle.Render("NOT"), formatter.BoldStyle.Render("NOW"), ) }, } var AppSecretInsertCommand = &cobra.Command{ Use: "insert [flags]", Aliases: []string{"i"}, Short: "Insert secret", Long: `This command inserts a secret into an app environment. 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).`, Args: cobra.MinimumNArgs(4), 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 { errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{errMsg}, 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 := args[3] if insertFromFile { raw, err := os.ReadFile(data) if err != nil { log.Fatalf("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.Infof("%s successfully stored on server", secretName) if storeInPass { if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { log.Fatal(err) } } }, } // 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.Infof("deleted %s successfully from server", secretName) if removeFromPass { if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { return err } log.Infof("deleted %s successfully from local pass store", secretName) } return nil } var AppSecretRmCommand = &cobra.Command{ Use: "remove [[secret] | --all] [flags]", Aliases: []string{"rm"}, Short: "Remove a secret", 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 { errMsg := fmt.Sprintf("autocomplete failed: %s", err) return []string{errMsg}, 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("cannot use [secret] and --all/-a together") } if len(args) != 2 && !rmAllSecrets { log.Fatal("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.Fatalf("%s doesn't exist on server?", secretToRm) } if !match { log.Fatal("no secrets to remove?") } }, } var AppSecretLsCommand = &cobra.Command{ Use: "list ", Aliases: []string{"ls"}, Short: "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{"NAME", "VERSION", "GENERATED NAME", "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) } 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("unable to render to JSON: %s", err) } fmt.Println(out) return } if err := formatter.PrintTable(table); err != nil { log.Fatal(err) } return } log.Warnf("no secrets stored for %s", app.Name) }, } var AppSecretCommand = &cobra.Command{ Use: "secret [cmd] [args] [flags]", Aliases: []string{"s"}, Short: "Manage app secrets", } var ( storeInPass bool insertFromFile bool trimInput bool rmAllSecrets bool generateAllSecrets bool removeFromPass bool ) func init() { AppSecretGenerateCommand.Flags().BoolVarP( &internal.MachineReadable, "machine", "m", false, "print machine-readable output", ) AppSecretGenerateCommand.Flags().BoolVarP( &storeInPass, "pass", "p", false, "store generated secrets in a local pass store", ) AppSecretGenerateCommand.Flags().BoolVarP( &internal.Chaos, "chaos", "C", false, "ignore uncommitted recipes changes", ) AppSecretGenerateCommand.Flags().BoolVarP( &generateAllSecrets, "all", "a", false, "generate all secrets", ) AppSecretInsertCommand.Flags().BoolVarP( &storeInPass, "pass", "p", false, "store generated secrets in a local pass store", ) AppSecretInsertCommand.Flags().BoolVarP( &insertFromFile, "file", "f", false, "treat input as a file", ) AppSecretInsertCommand.Flags().BoolVarP( &trimInput, "trim", "t", false, "trim input", ) AppSecretInsertCommand.Flags().BoolVarP( &internal.Chaos, "chaos", "C", false, "ignore uncommitted recipes changes", ) AppSecretRmCommand.Flags().BoolVarP( &rmAllSecrets, "all", "a", false, "remove all secrets", ) AppSecretRmCommand.Flags().BoolVarP( &removeFromPass, "pass", "p", false, "remove generated secrets from a local pass store", ) AppSecretRmCommand.Flags().BoolVarP( &internal.Chaos, "chaos", "C", false, "ignore uncommitted recipes changes", ) AppSecretLsCommand.Flags().BoolVarP( &internal.Chaos, "chaos", "C", false, "ignore uncommitted recipes changes", ) AppSecretLsCommand.Flags().BoolVarP( &internal.MachineReadable, "machine", "m", false, "print machine-readable output", ) }