402 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			402 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package app
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"strings"
 | |
| 
 | |
| 	"coopcloud.tech/abra/cli/internal"
 | |
| 	"coopcloud.tech/abra/pkg/app"
 | |
| 	"coopcloud.tech/abra/pkg/autocomplete"
 | |
| 	"coopcloud.tech/abra/pkg/config"
 | |
| 	"coopcloud.tech/abra/pkg/envfile"
 | |
| 	"coopcloud.tech/abra/pkg/secret"
 | |
| 
 | |
| 	appPkg "coopcloud.tech/abra/pkg/app"
 | |
| 	"coopcloud.tech/abra/pkg/client"
 | |
| 	"coopcloud.tech/abra/pkg/deploy"
 | |
| 	"coopcloud.tech/abra/pkg/dns"
 | |
| 	"coopcloud.tech/abra/pkg/formatter"
 | |
| 	"coopcloud.tech/abra/pkg/i18n"
 | |
| 	"coopcloud.tech/abra/pkg/lint"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	"coopcloud.tech/abra/pkg/upstream/stack"
 | |
| 	dockerClient "github.com/docker/docker/client"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| // translators: `abra app deploy` aliases. use a comma separated list of aliases with
 | |
| // no spaces in between
 | |
| var appDeployAliases = i18n.G("d")
 | |
| 
 | |
| var AppDeployCommand = &cobra.Command{
 | |
| 	// translators: `app deploy` command
 | |
| 	Use:     i18n.G("deploy <domain> [version] [flags]"),
 | |
| 	Aliases: strings.Split(appDeployAliases, ","),
 | |
| 	// translators: Short description for `app deploy` command
 | |
| 	Short: i18n.G("Deploy an app"),
 | |
| 	Long: i18n.G(`Deploy an app.
 | |
| 
 | |
| This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
 | |
| checkout as-is. Recipe commit hashes are also supported as values for
 | |
| "[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`),
 | |
| 	Example: i18n.G(`  # standard deployment
 | |
|   abra app deploy 1312.net
 | |
| 
 | |
|   # chaos deployment
 | |
|   abra app deploy 1312.net --chaos
 | |
|   
 | |
|   # deploy specific version
 | |
|   abra app deploy 1312.net 2.0.0+1.2.3
 | |
| 
 | |
|   # deploy a specific git hash
 | |
|   abra app deploy 1312.net 886db76d`),
 | |
| 	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:
 | |
| 			app, err := appPkg.Get(args[0])
 | |
| 			if err != nil {
 | |
| 				errMsg := i18n.G("autocomplete failed: %s", err)
 | |
| 				return []string{errMsg}, cobra.ShellCompDirectiveError
 | |
| 			}
 | |
| 			return autocomplete.RecipeVersionComplete(app.Recipe.Name)
 | |
| 		default:
 | |
| 			return nil, cobra.ShellCompDirectiveDefault
 | |
| 		}
 | |
| 	},
 | |
| 	Run: func(cmd *cobra.Command, args []string) {
 | |
| 		var (
 | |
| 			deployWarnMessages []string
 | |
| 			toDeployVersion    string
 | |
| 		)
 | |
| 
 | |
| 		app := internal.ValidateApp(args)
 | |
| 
 | |
| 		if err := validateArgsAndFlags(args); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		cl, err := client.New(app.Server)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))
 | |
| 
 | |
| 		deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
 | |
| 			log.Fatal(i18n.G("%s is already deployed", app.Name))
 | |
| 		}
 | |
| 
 | |
| 		toDeployVersion, err = getDeployVersion(args, deployMeta, app)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(i18n.G("get deploy version: %s", err))
 | |
| 		}
 | |
| 
 | |
| 		if !internal.Chaos {
 | |
| 			_, err = app.Recipe.EnsureVersion(toDeployVersion)
 | |
| 			if err != nil {
 | |
| 				log.Fatal(i18n.G("ensure recipe: %s", err))
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err := lint.LintForErrors(app.Recipe); err != nil {
 | |
| 			if internal.Chaos {
 | |
| 				log.Warn(err)
 | |
| 			} else {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err := validateSecrets(cl, app); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 		for k, v := range abraShEnv {
 | |
| 			app.Env[k] = v
 | |
| 		}
 | |
| 
 | |
| 		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		stackName := app.StackName()
 | |
| 		deployOpts := stack.Deploy{
 | |
| 			Composefiles: composeFiles,
 | |
| 			Namespace:    stackName,
 | |
| 			Prune:        false,
 | |
| 			ResolveImage: stack.ResolveImageAlways,
 | |
| 			Detach:       false,
 | |
| 		}
 | |
| 		compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		appPkg.ExposeAllEnv(stackName, compose, app.Env)
 | |
| 		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
 | |
| 		appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
 | |
| 		if internal.Chaos {
 | |
| 			appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
 | |
| 		}
 | |
| 		appPkg.SetUpdateLabel(compose, stackName, app.Env)
 | |
| 		appPkg.SetVersionLabel(compose, stackName, toDeployVersion)
 | |
| 
 | |
| 		envVars, err := appPkg.CheckEnv(app)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		for _, envVar := range envVars {
 | |
| 			if !envVar.Present {
 | |
| 				deployWarnMessages = append(deployWarnMessages,
 | |
| 					i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
 | |
| 				)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if !internal.NoDomainChecks {
 | |
| 			if domainName, ok := app.Env["DOMAIN"]; ok {
 | |
| 				if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
 | |
| 					log.Fatal(err)
 | |
| 				}
 | |
| 			} else {
 | |
| 				log.Debug(i18n.G("skipping domain checks, no DOMAIN=... configured"))
 | |
| 			}
 | |
| 		} else {
 | |
| 			log.Debug(i18n.G("skipping domain checks"))
 | |
| 		}
 | |
| 
 | |
| 		deployedVersion := config.NO_VERSION_DEFAULT
 | |
| 		if deployMeta.IsDeployed {
 | |
| 			deployedVersion = deployMeta.Version
 | |
| 		}
 | |
| 
 | |
| 		// Gather secrets
 | |
| 		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		// Gather configs
 | |
| 		configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, abraShEnv)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		// Gather images
 | |
| 		imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		// Show deploy overview
 | |
| 
 | |
| 		if err := internal.DeployOverview(
 | |
| 			app,
 | |
| 			deployedVersion,
 | |
| 			toDeployVersion,
 | |
| 			"",
 | |
| 			deployWarnMessages,
 | |
| 			strings.Join(secretInfo, "\n"),
 | |
| 			strings.Join(configInfo, "\n"),
 | |
| 			strings.Join(imageInfo, "\n"),
 | |
| 		); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 		// FIXME: just for debugging
 | |
| 		return
 | |
| 
 | |
| 		stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		serviceNames, err := appPkg.GetAppServiceNames(app.Name)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		f, err := app.Filters(true, false, serviceNames...)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if err := stack.RunDeploy(
 | |
| 			cl,
 | |
| 			deployOpts,
 | |
| 			compose,
 | |
| 			app.Name,
 | |
| 			app.Server,
 | |
| 			internal.DontWaitConverge,
 | |
| 			f,
 | |
| 		); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
 | |
| 		if ok && !internal.DontWaitConverge {
 | |
| 			log.Debug(i18n.G("run the following post-deploy commands: %s", postDeployCmds))
 | |
| 			if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
 | |
| 				log.Fatal(i18n.G("attempting to run post deploy commands, saw: %s", err))
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil {
 | |
| 			log.Fatal(i18n.G("writing recipe version failed: %s", err))
 | |
| 		}
 | |
| 	},
 | |
| }
 | |
| 
 | |
| func getLatestVersionOrCommit(app app.App) (string, error) {
 | |
| 	versions, err := app.Recipe.Tags()
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	if len(versions) > 0 && !internal.Chaos {
 | |
| 		return versions[len(versions)-1], nil
 | |
| 	}
 | |
| 
 | |
| 	head, err := app.Recipe.Head()
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return formatter.SmallSHA(head.String()), nil
 | |
| }
 | |
| 
 | |
| // validateArgsAndFlags ensures compatible args/flags.
 | |
| func validateArgsAndFlags(args []string) error {
 | |
| 	if len(args) == 2 && args[1] != "" && internal.Chaos {
 | |
| 		return errors.New(i18n.G("cannot use [version] and --chaos together"))
 | |
| 	}
 | |
| 
 | |
| 	if len(args) == 2 && args[1] != "" && internal.DeployLatest {
 | |
| 		return errors.New(i18n.G("cannot use [version] and --latest together"))
 | |
| 	}
 | |
| 
 | |
| 	if internal.DeployLatest && internal.Chaos {
 | |
| 		return errors.New(i18n.G("cannot use --chaos and --latest together"))
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func validateSecrets(cl *dockerClient.Client, app app.App) error {
 | |
| 	secStats, err := secret.PollSecretsStatus(cl, app)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	for _, secStat := range secStats {
 | |
| 		if !secStat.CreatedOnRemote {
 | |
| 			return errors.New(i18n.G("secret not generated: %s", secStat.LocalName))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App) (string, error) {
 | |
| 	// Chaos mode overrides everything
 | |
| 	if internal.Chaos {
 | |
| 		v, err := app.Recipe.ChaosVersion()
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		log.Debug(i18n.G("version: taking chaos version: %s", v))
 | |
| 		return v, nil
 | |
| 	}
 | |
| 
 | |
| 	// Check if the deploy version is set with a cli argument
 | |
| 	if len(cliArgs) == 2 && cliArgs[1] != "" {
 | |
| 		log.Debug(i18n.G("version: taking version from cli arg: %s", cliArgs[1]))
 | |
| 		return cliArgs[1], nil
 | |
| 	}
 | |
| 
 | |
| 	// Check if the recipe has a version in the .env file
 | |
| 	if app.Recipe.EnvVersion != "" && !internal.DeployLatest {
 | |
| 		if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
 | |
| 			return "", errors.New(i18n.G("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw))
 | |
| 		}
 | |
| 		log.Debug(i18n.G("version: taking version from .env file: %s", app.Recipe.EnvVersion))
 | |
| 		return app.Recipe.EnvVersion, nil
 | |
| 	}
 | |
| 
 | |
| 	// Take deployed version
 | |
| 	if deployMeta.IsDeployed && !internal.DeployLatest {
 | |
| 		log.Debug(i18n.G("version: taking deployed version: %s", deployMeta.Version))
 | |
| 		return deployMeta.Version, nil
 | |
| 	}
 | |
| 
 | |
| 	v, err := getLatestVersionOrCommit(app)
 | |
| 	log.Debug(i18n.G("version: taking new recipe version: %s", v))
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return v, nil
 | |
| }
 | |
| 
 | |
| func init() {
 | |
| 	AppDeployCommand.Flags().BoolVarP(
 | |
| 		&internal.Chaos,
 | |
| 		i18n.G("chaos"),
 | |
| 		i18n.G("C"),
 | |
| 		false,
 | |
| 		i18n.G("ignore uncommitted recipes changes"),
 | |
| 	)
 | |
| 
 | |
| 	AppDeployCommand.Flags().BoolVarP(
 | |
| 		&internal.Force,
 | |
| 		i18n.G("force"),
 | |
| 		i18n.G("f"),
 | |
| 		false,
 | |
| 		i18n.G("perform action without further prompt"),
 | |
| 	)
 | |
| 
 | |
| 	AppDeployCommand.Flags().BoolVarP(
 | |
| 		&internal.NoDomainChecks,
 | |
| 		i18n.G("no-domain-checks"),
 | |
| 		i18n.G("D"),
 | |
| 		false,
 | |
| 		i18n.G("disable public DNS checks"),
 | |
| 	)
 | |
| 
 | |
| 	AppDeployCommand.Flags().BoolVarP(
 | |
| 		&internal.DontWaitConverge,
 | |
| 		i18n.G("no-converge-checks"),
 | |
| 		i18n.G("c"),
 | |
| 		false,
 | |
| 		i18n.G("disable converge logic checks"),
 | |
| 	)
 | |
| 
 | |
| 	AppDeployCommand.PersistentFlags().BoolVarP(
 | |
| 		&internal.DeployLatest,
 | |
| 		"latest",
 | |
| 		"l",
 | |
| 		false,
 | |
| 		i18n.G("deploy latest recipe version"),
 | |
| 	)
 | |
| }
 |