563 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			563 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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 <domain> [[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) <= 2 && !generateAllSecrets {
 | |
| 			log.Fatal("missing arguments [secret]/[version] or '--all'")
 | |
| 		}
 | |
| 
 | |
| 		if len(args) > 2 && 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 <domain> <secret> <version> <data> [flags]",
 | |
| 	Aliases: []string{"i"},
 | |
| 	Short:   "Insert secret",
 | |
| 	Long: `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: `  # 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`,
 | |
| 	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]
 | |
| 
 | |
| 		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.Fatalf("no secret %s available for recipe %s?", name, app.Recipe.Name)
 | |
| 		}
 | |
| 
 | |
| 		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 <domain> [[secret] | --all] [flags]",
 | |
| 	Aliases: []string{"rm"},
 | |
| 	Short:   "Remove a secret",
 | |
| 	Long: `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: "  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 {
 | |
| 					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 <domain>",
 | |
| 	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",
 | |
| 	)
 | |
| }
 |