forked from toolshed/abra
1111b69f12
I had a lot of failures for pulling the docker images lately, so I was looking for a way to connect using docker login. This PR sends the docker login credentials from the host to the swarm server.
460 lines
12 KiB
Go
460 lines
12 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"
|
|
"coopcloud.tech/tagcmp"
|
|
|
|
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)
|
|
}
|
|
|
|
isChaosCommit, err := app.Recipe.IsChaosCommit(toDeployVersion)
|
|
if err != nil {
|
|
log.Fatal(i18n.G("unable to determine if %s is a chaos commit: %s", toDeployVersion, err))
|
|
}
|
|
|
|
if !isChaosCommit && !tagcmp.IsParsable(toDeployVersion) {
|
|
log.Fatal(i18n.G("unable to parse deploy version: %s", toDeployVersion))
|
|
}
|
|
|
|
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,
|
|
SendRegistryAuth: true,
|
|
}
|
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
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)
|
|
|
|
newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion)
|
|
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion)
|
|
|
|
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,
|
|
internal.NoInput,
|
|
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) {
|
|
recipeVersions, warnings, err := app.Recipe.GetRecipeVersions()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, warning := range warnings {
|
|
log.Warn(warning)
|
|
}
|
|
|
|
if len(recipeVersions) > 0 && !internal.Chaos {
|
|
latest := recipeVersions[len(recipeVersions)-1]
|
|
for tag := range latest {
|
|
log.Debug(i18n.G("selected latest recipe version: %s (from %d available versions)", tag, len(recipeVersions)))
|
|
return tag, 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"),
|
|
)
|
|
}
|