package app import ( "context" "fmt" "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/dns" "coopcloud.tech/abra/pkg/formatter" "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" ) var AppDeployCommand = &cobra.Command{ Use: "deploy [version] [flags]", Aliases: []string{"d"}, Short: "Deploy an app", Long: `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: ` # 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 := fmt.Sprintf("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.Debugf("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.Fatalf("%s is already deployed", app.Name) } toDeployVersion, err = getDeployVersion(args, deployMeta, app) if err != nil { log.Fatal(fmt.Errorf("get deploy version: %s", err)) } if !internal.Chaos { _, err = app.Recipe.EnsureVersion(toDeployVersion) if err != nil { log.Fatalf("ensure recipe: %s", err) } } if err := lint.LintForErrors(app.Recipe); err != nil { 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, fmt.Sprintf("%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("skipping domain checks, no DOMAIN=... configured") } } else { log.Debug("skipping domain checks") } deployedVersion := config.NO_VERSION_DEFAULT if deployMeta.IsDeployed { deployedVersion = deployMeta.Version } if err := internal.DeployOverview( app, deployedVersion, toDeployVersion, "", deployWarnMessages, ); err != nil { log.Fatal(err) } stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName) if err != nil { log.Fatal(err) } log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout) if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil { log.Fatal(err) } postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"] if ok && !internal.DontWaitConverge { log.Debugf("run the following post-deploy commands: %s", postDeployCmds) if err := internal.PostCmds(cl, app, postDeployCmds); err != nil { log.Fatalf("attempting to run post deploy commands, saw: %s", err) } } if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil { log.Fatalf("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 fmt.Errorf("cannot use [version] and --chaos 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 fmt.Errorf("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.Debugf("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.Debugf("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.IgnoreEnvVersion { if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") { return "", fmt.Errorf("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw) } log.Debugf("version: taking version from .env file: %s", app.Recipe.EnvVersion) return app.Recipe.EnvVersion, nil } // Take deployed version if deployMeta.IsDeployed { log.Debugf("version: taking deployed version: %s", deployMeta.Version) return deployMeta.Version, nil } v, err := getLatestVersionOrCommit(app) log.Debugf("version: taking new recipe version: %s", v) if err != nil { return "", err } return v, nil } func init() { AppDeployCommand.Flags().BoolVarP( &internal.Chaos, "chaos", "C", false, "ignore uncommitted recipes changes", ) AppDeployCommand.Flags().BoolVarP( &internal.Force, "force", "f", false, "perform action without further prompt", ) AppDeployCommand.Flags().BoolVarP( &internal.NoDomainChecks, "no-domain-checks", "D", false, "disable public DNS checks", ) AppDeployCommand.Flags().BoolVarP( &internal.DontWaitConverge, "no-converge-checks", "c", false, "disable converge logic checks", ) }