forked from toolshed/abra
		
	Fixes toolshed/abra#494 Reviewed-on: toolshed/abra#495 Co-authored-by: p4u1 <p4u1_f4u1@riseup.net> Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
		
			
				
	
	
		
			378 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			378 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package app
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 
 | |
| 	"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/log"
 | |
| 	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | |
| 	"coopcloud.tech/abra/pkg/secret"
 | |
| 	"github.com/AlecAivazis/survey/v2"
 | |
| 	"github.com/charmbracelet/lipgloss/table"
 | |
| 	dockerClient "github.com/docker/docker/client"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| var appNewDescription = `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.`
 | |
| 
 | |
| var AppNewCommand = &cobra.Command{
 | |
| 	Use:     "new [recipe] [version] [flags]",
 | |
| 	Aliases: []string{"n"},
 | |
| 	Short:   "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("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 err := ensureServerFlag(); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if err := ensureDomainFlag(recipe, newAppServer); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		sanitisedAppName := appPkg.SanitiseAppName(appDomain)
 | |
| 		log.Debugf("%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
 | |
| 		var secretsTable *table.Table
 | |
| 		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)
 | |
| 			}
 | |
| 
 | |
| 			secretsTable, err = formatter.CreateTable()
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			headers := []string{"NAME", "VALUE"}
 | |
| 			secretsTable.Headers(headers...)
 | |
| 
 | |
| 			for name, val := range appSecrets {
 | |
| 				secretsTable.Row(name, val)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if newAppServer == "default" {
 | |
| 			newAppServer = "local"
 | |
| 		}
 | |
| 
 | |
| 		log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion)
 | |
| 
 | |
| 		if len(appSecrets) > 0 {
 | |
| 			rows := [][]string{}
 | |
| 			for k, v := range appSecrets {
 | |
| 				rows = append(rows, []string{k, v})
 | |
| 			}
 | |
| 
 | |
| 			overview := formatter.CreateOverview("SECRETS OVERVIEW", rows)
 | |
| 
 | |
| 			fmt.Println(overview)
 | |
| 
 | |
| 			log.Warnf(
 | |
| 				"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.Recipe.IsDirty(); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		toWriteVersion := recipeVersion
 | |
| 		if internal.Chaos || app.Recipe.Dirty {
 | |
| 			toWriteVersion = chaosVersion
 | |
| 		}
 | |
| 
 | |
| 		if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
 | |
| 			log.Fatalf("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.Debugf("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: "Specify app domain",
 | |
| 			Default: fmt.Sprintf("%s.%s", recipe.Name, server),
 | |
| 		}
 | |
| 		if err := survey.AskOne(prompt, &appDomain); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if appDomain == "" {
 | |
| 		return fmt.Errorf("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.Debugf("%s has no secrets to generate, skipping...", recipeName)
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	if !generateSecrets && !internal.NoInput {
 | |
| 		prompt := &survey.Confirm{
 | |
| 			Message: "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 newAppServer == "" && !internal.NoInput {
 | |
| 		prompt := &survey.Select{
 | |
| 			Message: "Select app server:",
 | |
| 			Options: servers,
 | |
| 		}
 | |
| 		if err := survey.AskOne(prompt, &newAppServer); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if newAppServer == "" {
 | |
| 		return fmt.Errorf("no server provided")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	newAppServer    string
 | |
| 	appDomain       string
 | |
| 	saveInPass      bool
 | |
| 	generateSecrets bool
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	AppNewCommand.Flags().StringVarP(
 | |
| 		&newAppServer,
 | |
| 		"server",
 | |
| 		"s",
 | |
| 		"",
 | |
| 		"specify server for new app",
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.RegisterFlagCompletionFunc(
 | |
| 		"server",
 | |
| 		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 | |
| 			return autocomplete.ServerNameComplete()
 | |
| 		},
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.Flags().StringVarP(
 | |
| 		&appDomain,
 | |
| 		"domain",
 | |
| 		"D",
 | |
| 		"",
 | |
| 		"domain name for app",
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.Flags().BoolVarP(
 | |
| 		&saveInPass,
 | |
| 		"pass",
 | |
| 		"p",
 | |
| 		false,
 | |
| 		"store secrets in a local pass store",
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.Flags().BoolVarP(
 | |
| 		&generateSecrets,
 | |
| 		"secrets",
 | |
| 		"S",
 | |
| 		false,
 | |
| 		"automatically generate secrets",
 | |
| 	)
 | |
| 
 | |
| 	AppNewCommand.Flags().BoolVarP(
 | |
| 		&internal.Chaos,
 | |
| 		"chaos",
 | |
| 		"C",
 | |
| 		false,
 | |
| 		"ignore uncommitted recipes changes",
 | |
| 	)
 | |
| 
 | |
| }
 |