334 lines
8.5 KiB
Go
334 lines
8.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
"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"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var AppDeployCommand = &cobra.Command{
|
|
Use: "deploy <app> [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 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 warnMessages []string
|
|
|
|
app := internal.ValidateApp(args)
|
|
stackName := app.StackName()
|
|
|
|
ok, err := validateChaosXORVersion(args)
|
|
if !ok {
|
|
log.Fatalf(err.Error())
|
|
}
|
|
|
|
specificVersion := getSpecifiedVersion(args)
|
|
|
|
if specificVersion != "" {
|
|
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
|
|
app.Recipe.Version = specificVersion
|
|
}
|
|
|
|
if specificVersion == "" && app.Recipe.Version != "" && !internal.Chaos {
|
|
log.Debugf("retrieved %s as version from env file", app.Recipe.Version)
|
|
specificVersion = app.Recipe.Version
|
|
}
|
|
|
|
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if err := lint.LintForErrors(app.Recipe); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
log.Debugf("checking whether %s is already deployed", stackName)
|
|
|
|
cl, err := client.New(app.Server)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// NOTE(d1): handles "[version] as git hash" use case
|
|
var isChaosCommit bool
|
|
|
|
// NOTE(d1): check out specific version before dealing with secrets. This
|
|
// is because we need to deal with GetComposeFiles under the hood and these
|
|
// files change from version to version which therefore affects which
|
|
// secrets might be generated
|
|
toDeployVersion := deployMeta.Version
|
|
if specificVersion != "" {
|
|
toDeployVersion = specificVersion
|
|
log.Debugf("choosing %s as version to deploy", toDeployVersion)
|
|
|
|
var err error
|
|
isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if isChaosCommit {
|
|
log.Debugf("assuming '%s' is a chaos commit", toDeployVersion)
|
|
internal.Chaos = true
|
|
}
|
|
}
|
|
|
|
secStats, err := secret.PollSecretsStatus(cl, app)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
for _, secStat := range secStats {
|
|
if !secStat.CreatedOnRemote {
|
|
log.Fatalf("unable to deploy, secrets not generated (%s)?", secStat.LocalName)
|
|
}
|
|
}
|
|
|
|
if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
|
|
log.Fatalf("%s is already deployed", app.Name)
|
|
}
|
|
|
|
if !internal.Chaos && specificVersion == "" {
|
|
versions, err := app.Recipe.Tags()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if len(versions) > 0 && !internal.Chaos {
|
|
toDeployVersion = versions[len(versions)-1]
|
|
log.Debugf("choosing %s as version to deploy", toDeployVersion)
|
|
if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
} else {
|
|
head, err := app.Recipe.Head()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
toDeployVersion = formatter.SmallSHA(head.String())
|
|
}
|
|
}
|
|
|
|
toDeployChaosVersion := config.CHAOS_DEFAULT
|
|
if internal.Chaos {
|
|
if isChaosCommit {
|
|
toDeployChaosVersion = specificVersion
|
|
versionLabelLocal, err := app.Recipe.GetVersionLabelLocal()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
toDeployVersion = versionLabelLocal
|
|
} else {
|
|
var err error
|
|
toDeployChaosVersion, err = app.Recipe.ChaosVersion()
|
|
if 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)
|
|
}
|
|
|
|
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)
|
|
appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion)
|
|
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
|
|
|
envVars, err := appPkg.CheckEnv(app)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
for _, envVar := range envVars {
|
|
if !envVar.Present {
|
|
warnMessages = append(warnMessages,
|
|
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
|
|
)
|
|
}
|
|
}
|
|
|
|
if !internal.NoDomainChecks {
|
|
domainName, ok := app.Env["DOMAIN"]
|
|
if ok {
|
|
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
} else {
|
|
log.Debug("skipping domain checks as no DOMAIN=... configured for app")
|
|
}
|
|
} else {
|
|
log.Debug("skipping domain checks as requested")
|
|
}
|
|
|
|
deployedVersion := config.NO_VERSION_DEFAULT
|
|
if deployMeta.IsDeployed {
|
|
deployedVersion = deployMeta.Version
|
|
}
|
|
|
|
if err := internal.DeployOverview(
|
|
app,
|
|
warnMessages,
|
|
deployedVersion,
|
|
deployMeta.ChaosVersion,
|
|
toDeployVersion,
|
|
toDeployChaosVersion); 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 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)
|
|
}
|
|
}
|
|
|
|
app.Recipe.Version = toDeployVersion
|
|
if toDeployChaosVersion != config.CHAOS_DEFAULT {
|
|
app.Recipe.Version = toDeployChaosVersion
|
|
}
|
|
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
|
|
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
|
|
log.Fatalf("writing new recipe version in env file: %s", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
// validateChaosXORVersion xor checks version/chaos mode
|
|
func validateChaosXORVersion(args []string) (bool, error) {
|
|
if getSpecifiedVersion(args) != "" && internal.Chaos {
|
|
return false, errors.New("cannot use [version] and --chaos together")
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// getSpecifiedVersion retrieves the specific version if available
|
|
func getSpecifiedVersion(args []string) string {
|
|
if len(args) >= 2 {
|
|
return args[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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,
|
|
"do not wait for converge logic checks",
|
|
)
|
|
}
|