forked from toolshed/abra
		
	
		
			
				
	
	
		
			438 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			438 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package app
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"coopcloud.tech/abra/cli/internal"
 | 
						|
	"coopcloud.tech/abra/pkg/autocomplete"
 | 
						|
	"coopcloud.tech/abra/pkg/config"
 | 
						|
	"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(err)
 | 
						|
		}
 | 
						|
 | 
						|
		if !internal.Chaos {
 | 
						|
			isChaosCommit, err := app.Recipe.EnsureVersion(toDeployVersion)
 | 
						|
			if err != nil {
 | 
						|
				log.Fatal(i18n.G("ensure recipe: %s", err))
 | 
						|
			}
 | 
						|
			if isChaosCommit {
 | 
						|
				log.Warnf(i18n.G("version '%s' appears to be a chaos commit, but --chaos/-C was not provided", toDeployVersion))
 | 
						|
				internal.Chaos = true
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
		}
 | 
						|
 | 
						|
		if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		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)
 | 
						|
		}
 | 
						|
 | 
						|
		versionLabel := toDeployVersion
 | 
						|
		if internal.Chaos {
 | 
						|
			for _, service := range compose.Services {
 | 
						|
				if service.Name == "app" {
 | 
						|
					labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
 | 
						|
					// NOTE(d1): keep non-chaos version labbeling when doing chaos ops
 | 
						|
					versionLabel = service.Deploy.Labels[labelKey]
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		appPkg.SetVersionLabel(compose, stackName, versionLabel)
 | 
						|
 | 
						|
		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.MISSING_DEFAULT
 | 
						|
		if deployMeta.IsDeployed {
 | 
						|
			deployedVersion = deployMeta.Version
 | 
						|
			if deployMeta.IsChaos {
 | 
						|
				deployedVersion = deployMeta.ChaosVersion
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// Gather secrets
 | 
						|
		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		// Gather configs
 | 
						|
		configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		// Gather images
 | 
						|
		imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		// Show deploy overview
 | 
						|
		if err := internal.DeployOverview(
 | 
						|
			app,
 | 
						|
			deployedVersion,
 | 
						|
			toDeployVersion,
 | 
						|
			"",
 | 
						|
			deployWarnMessages,
 | 
						|
			secretInfo,
 | 
						|
			configInfo,
 | 
						|
			imageInfo,
 | 
						|
		); err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		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 appPkg.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 appPkg.App) error {
 | 
						|
	composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	secretsConfig, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	secStats, err := secret.PollSecretsStatus(cl, app)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, secStat := range secStats {
 | 
						|
		if !secStat.CreatedOnRemote {
 | 
						|
			secretConfig := secretsConfig[secStat.LocalName]
 | 
						|
			if secretConfig.SkipGenerate {
 | 
						|
				return errors.New(i18n.G("secret not inserted (#generate=false): %s", secStat.LocalName))
 | 
						|
			}
 | 
						|
			return errors.New(i18n.G("secret not generated: %s", secStat.LocalName))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app appPkg.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") {
 | 
						|
			// NOTE(d1): use double-line 5 spaces ("FATA ") trick to make a more
 | 
						|
			// informative error message. it's ugly but that's our logging situation
 | 
						|
			// atm
 | 
						|
			return "", errors.New(i18n.G(`cannot redeploy previous chaos version (%s), did you mean to use "--chaos"?
 | 
						|
     to return to a regular release, specify a release tag, commit SHA or use "--latest"`,
 | 
						|
				formatter.BoldDirtyDefault(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,
 | 
						|
		i18n.G("latest"),
 | 
						|
		i18n.G("l"),
 | 
						|
		false,
 | 
						|
		i18n.G("deploy latest recipe version"),
 | 
						|
	)
 | 
						|
 | 
						|
	AppDeployCommand.Flags().BoolVarP(
 | 
						|
		&internal.ShowUnchanged,
 | 
						|
		i18n.G("show-unchanged"),
 | 
						|
		i18n.G("U"),
 | 
						|
		false,
 | 
						|
		i18n.G("show all configs & images, including unchanged ones"),
 | 
						|
	)
 | 
						|
}
 |