package app import ( "context" "errors" "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 [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)) } versionIsChaos := false if !internal.Chaos { var err error versionIsChaos, err = app.Recipe.EnsureVersion(toDeployVersion) if err != nil { log.Fatal(i18n.G("ensure recipe: %s", err)) } if versionIsChaos { log.Warnf(i18n.G("version '%s' appears to be a chaos commit, but --chaos/-C was not provided", toDeployVersion)) } } 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 || versionIsChaos) 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, 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 { 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 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") { 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, 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"), ) }