forked from toolshed/abra
		
	
		
			
				
	
	
		
			379 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			379 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package app
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 
 | |
| 	"coopcloud.tech/abra/cli/internal"
 | |
| 	"coopcloud.tech/abra/pkg/app"
 | |
| 	appPkg "coopcloud.tech/abra/pkg/app"
 | |
| 	"coopcloud.tech/abra/pkg/autocomplete"
 | |
| 	"coopcloud.tech/abra/pkg/client"
 | |
| 	"coopcloud.tech/abra/pkg/config"
 | |
| 	"coopcloud.tech/abra/pkg/formatter"
 | |
| 	"coopcloud.tech/abra/pkg/i18n"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | |
| 	"coopcloud.tech/abra/pkg/secret"
 | |
| 	"github.com/AlecAivazis/survey/v2"
 | |
| 	dockerClient "github.com/docker/docker/client"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| var appNewDescription = i18n.G(`Creates a new app from a default recipe.
 | |
| 
 | |
| This new app configuration is stored in your $ABRA_DIR directory under the
 | |
| appropriate server.
 | |
| 
 | |
| This command does not deploy your app for you. You will need to run "abra app
 | |
| deploy <domain>" to do so.
 | |
| 
 | |
| You can see what recipes are available (i.e. values for the [recipe] argument)
 | |
| by running "abra recipe ls".
 | |
| 
 | |
| Recipe commit hashes are supported values for "[version]".
 | |
| 
 | |
| Passing the "--secrets/-S" flag will automatically generate secrets for your
 | |
| app and store them encrypted at rest on the chosen target server. These
 | |
| generated secrets are only visible at generation time, so please take care to
 | |
| store them somewhere safe.
 | |
| 
 | |
| You can use the "--pass/-P" to store these generated passwords locally in a
 | |
| pass store (see passwordstore.org for more). The pass command must be available
 | |
| on your $PATH.`)
 | |
| 
 | |
| // translators: `abra app new` aliases. use a comma separated list of aliases with
 | |
| // no spaces in between
 | |
| var appNewAliases = i18n.G("n")
 | |
| 
 | |
| var AppNewCommand = &cobra.Command{
 | |
| 	// translators: `app new` command
 | |
| 	Use:     i18n.G("new [recipe] [version] [flags]"),
 | |
| 	Aliases: strings.Split(appNewAliases, ","),
 | |
| 	// translators: Short description for `app new` command
 | |
| 	Short: i18n.G("Create a new app"),
 | |
| 	Long:  appNewDescription,
 | |
| 	Args:  cobra.RangeArgs(0, 2),
 | |
| 	ValidArgsFunction: func(
 | |
| 		cmd *cobra.Command,
 | |
| 		args []string,
 | |
| 		toComplete string) ([]string, cobra.ShellCompDirective) {
 | |
| 		switch l := len(args); l {
 | |
| 		case 0:
 | |
| 			return autocomplete.RecipeNameComplete()
 | |
| 		case 1:
 | |
| 			recipe := internal.ValidateRecipe(args, cmd.Name())
 | |
| 			return autocomplete.RecipeVersionComplete(recipe.Name)
 | |
| 		default:
 | |
| 			return nil, cobra.ShellCompDirectiveDefault
 | |
| 		}
 | |
| 	},
 | |
| 	Run: func(cmd *cobra.Command, args []string) {
 | |
| 		recipe := internal.ValidateRecipe(args, cmd.Name())
 | |
| 
 | |
| 		if len(args) == 2 && internal.Chaos {
 | |
| 			log.Fatal(i18n.G("cannot use [version] and --chaos together"))
 | |
| 		}
 | |
| 
 | |
| 		var recipeVersion string
 | |
| 		if len(args) == 2 {
 | |
| 			recipeVersion = args[1]
 | |
| 		}
 | |
| 
 | |
| 		chaosVersion := config.CHAOS_DEFAULT
 | |
| 		if internal.Chaos {
 | |
| 			var err error
 | |
| 			chaosVersion, err = recipe.ChaosVersion()
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			recipeVersion = chaosVersion
 | |
| 		} else {
 | |
| 			if err := recipe.EnsureIsClean(); err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			var recipeVersions recipePkg.RecipeVersions
 | |
| 			if recipeVersion == "" {
 | |
| 				var err error
 | |
| 				recipeVersions, _, err = recipe.GetRecipeVersions()
 | |
| 				if err != nil {
 | |
| 					log.Fatal(err)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if len(recipeVersions) > 0 {
 | |
| 				latest := recipeVersions[len(recipeVersions)-1]
 | |
| 				for tag := range latest {
 | |
| 					recipeVersion = tag
 | |
| 				}
 | |
| 
 | |
| 				if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
 | |
| 					log.Fatal(err)
 | |
| 				}
 | |
| 			} else {
 | |
| 				if err := recipe.EnsureLatest(); err != nil {
 | |
| 					log.Fatal(err)
 | |
| 				}
 | |
| 
 | |
| 				if recipeVersion == "" {
 | |
| 					head, err := recipe.Head()
 | |
| 					if err != nil {
 | |
| 						log.Fatal(i18n.G("failed to retrieve latest commit for %s: %s", recipe.Name, err))
 | |
| 					}
 | |
| 
 | |
| 					recipeVersion = formatter.SmallSHA(head.String())
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err := ensureServerFlag(); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if err := ensureDomainFlag(recipe, newAppServer); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		sanitisedAppName := appPkg.SanitiseAppName(appDomain)
 | |
| 		log.Debug(i18n.G("%s sanitised as %s for new app", appDomain, sanitisedAppName))
 | |
| 
 | |
| 		if err := appPkg.TemplateAppEnvSample(
 | |
| 			recipe,
 | |
| 			appDomain,
 | |
| 			newAppServer,
 | |
| 			appDomain,
 | |
| 		); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		var appSecrets AppSecrets
 | |
| 		if generateSecrets {
 | |
| 			sampleEnv, err := recipe.SampleEnv()
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			composeFiles, err := recipe.GetComposeFiles(sampleEnv)
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			secretsConfig, err := secret.ReadSecretsConfig(
 | |
| 				recipe.SampleEnvPath,
 | |
| 				composeFiles,
 | |
| 				appPkg.StackName(appDomain),
 | |
| 			)
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			cl, err := client.New(newAppServer)
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if newAppServer == "default" {
 | |
| 			newAppServer = "local"
 | |
| 		}
 | |
| 
 | |
| 		log.Info(i18n.G("%s created (version: %s)", appDomain, recipeVersion))
 | |
| 
 | |
| 		if len(appSecrets) > 0 {
 | |
| 			rows := [][]string{}
 | |
| 			for k, v := range appSecrets {
 | |
| 				rows = append(rows, []string{k, v})
 | |
| 			}
 | |
| 
 | |
| 			overview := formatter.CreateOverview(i18n.G("SECRETS OVERVIEW"), rows)
 | |
| 
 | |
| 			fmt.Println(overview)
 | |
| 
 | |
| 			log.Warn(i18n.G(
 | |
| 				"secrets are %s shown again, please save them %s",
 | |
| 				formatter.BoldUnderlineStyle.Render("NOT"),
 | |
| 				formatter.BoldUnderlineStyle.Render("NOW"),
 | |
| 			))
 | |
| 		}
 | |
| 
 | |
| 		app, err := app.Get(appDomain)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
 | |
| 			log.Fatal(i18n.G("writing recipe version failed: %s", err))
 | |
| 		}
 | |
| 	},
 | |
| }
 | |
| 
 | |
| // AppSecrets represents all app secrest
 | |
| type AppSecrets map[string]string
 | |
| 
 | |
| // createSecrets creates all secrets for a new app.
 | |
| func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
 | |
| 	// NOTE(d1): trim to match app.StackName() implementation
 | |
| 	if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH {
 | |
| 		log.Debug(i18n.G("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]))
 | |
| 		sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
 | |
| 	}
 | |
| 
 | |
| 	secrets, err := secret.GenerateSecrets(cl, secretsConfig, newAppServer)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if saveInPass {
 | |
| 		for secretName := range secrets {
 | |
| 			secretValue := secrets[secretName]
 | |
| 			if err := secret.PassInsertSecret(
 | |
| 				secretValue,
 | |
| 				secretName,
 | |
| 				appDomain,
 | |
| 				newAppServer,
 | |
| 			); err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return secrets, nil
 | |
| }
 | |
| 
 | |
| // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
 | |
| func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
 | |
| 	if appDomain == "" && !internal.NoInput {
 | |
| 		prompt := &survey.Input{
 | |
| 			Message: i18n.G("Specify app domain"),
 | |
| 			Default: fmt.Sprintf("%s.%s", recipe.Name, server),
 | |
| 		}
 | |
| 		if err := survey.AskOne(prompt, &appDomain); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if appDomain == "" {
 | |
| 		return errors.New(i18n.G("no domain provided"))
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // promptForSecrets asks if we should generate secrets for a new app.
 | |
| func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
 | |
| 	if len(secretsConfig) == 0 {
 | |
| 		log.Debug(i18n.G("%s has no secrets to generate, skipping...", recipeName))
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	if !generateSecrets && !internal.NoInput {
 | |
| 		prompt := &survey.Confirm{
 | |
| 			Message: i18n.G("Generate app secrets?"),
 | |
| 		}
 | |
| 		if err := survey.AskOne(prompt, &generateSecrets); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ensureServerFlag checks if the server flag was used. if not, asks the user for it.
 | |
| func ensureServerFlag() error {
 | |
| 	servers, err := config.GetServers()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if len(servers) == 1 {
 | |
| 		newAppServer = servers[0]
 | |
| 		log.Info(i18n.G("single server detected, choosing %s automatically", newAppServer))
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	if newAppServer == "" && !internal.NoInput {
 | |
| 		prompt := &survey.Select{
 | |
| 			Message: i18n.G("Select app server:"),
 | |
| 			Options: servers,
 | |
| 		}
 | |
| 		if err := survey.AskOne(prompt, &newAppServer); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if newAppServer == "" {
 | |
| 		return errors.New(i18n.G("no server provided"))
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	newAppServer    string
 | |
| 	appDomain       string
 | |
| 	saveInPass      bool
 | |
| 	generateSecrets bool
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	AppNewCommand.Flags().StringVarP(
 | |
| 		&newAppServer,
 | |
| 		i18n.G("server"),
 | |
| 		i18n.G("s"),
 | |
| 		"",
 | |
| 		i18n.G("specify server for new app"),
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.RegisterFlagCompletionFunc(
 | |
| 		i18n.G("server"),
 | |
| 		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 | |
| 			return autocomplete.ServerNameComplete()
 | |
| 		},
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.Flags().StringVarP(
 | |
| 		&appDomain,
 | |
| 		i18n.G("domain"),
 | |
| 		i18n.G("D"),
 | |
| 		"",
 | |
| 		i18n.G("domain name for app"),
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.Flags().BoolVarP(
 | |
| 		&saveInPass,
 | |
| 		i18n.G("pass"),
 | |
| 		i18n.G("p"),
 | |
| 		false,
 | |
| 		i18n.G("store secrets in a local pass store"),
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.Flags().BoolVarP(
 | |
| 		&generateSecrets,
 | |
| 		i18n.G("secrets"),
 | |
| 		i18n.G("S"),
 | |
| 		false,
 | |
| 		i18n.G("automatically generate secrets"),
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.Flags().BoolVarP(
 | |
| 		&internal.Chaos,
 | |
| 		i18n.G("chaos"),
 | |
| 		i18n.G("C"),
 | |
| 		false,
 | |
| 		i18n.G("ignore uncommitted recipes changes"),
 | |
| 	)
 | |
| 
 | |
| }
 |