406 lines
10 KiB
Go
406 lines
10 KiB
Go
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 <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))
|
|
}
|
|
|
|
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"),
|
|
)
|
|
}
|