refactor/fix: deploy/upgrade/rollback

See coop-cloud/abra#461
This commit is contained in:
2025-01-01 19:15:22 +01:00
parent 5975be6870
commit b0cd8ccbb9
85 changed files with 783 additions and 7118 deletions

View File

@ -78,7 +78,7 @@ var AppBackupDownloadCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
@ -143,7 +143,7 @@ var AppBackupCreateCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -36,7 +36,7 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -92,7 +92,7 @@ does not).`,
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
@ -199,7 +199,7 @@ var AppCmdListCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -49,7 +49,7 @@ var AppCpCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -2,10 +2,10 @@ package app
import (
"context"
"errors"
"fmt"
"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"
@ -18,6 +18,7 @@ import (
"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"
)
@ -27,9 +28,9 @@ var AppDeployCommand = &cobra.Command{
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.`,
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
@ -61,29 +62,20 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
}
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
var (
deployWarnMessages []string
toDeployVersion string
isChaosCommit bool
toDeployChaosVersion = config.CHAOS_DEFAULT
)
app := internal.ValidateApp(args)
stackName := app.StackName()
ok, err := validateChaosXORVersion(args)
if !ok {
log.Fatalf(err.Error())
if err := validateArgsAndFlags(args); err != nil {
log.Fatal(err)
}
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 {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
@ -91,96 +83,66 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
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)
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)
}
// 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(args) == 2 && args[1] != "" {
toDeployVersion = args[1]
}
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())
if !deployMeta.IsDeployed &&
toDeployVersion == "" &&
app.Recipe.EnvVersion != "" && !internal.IgnoreEnvVersion {
log.Debugf("new deployment, choosing .env version: %s", app.Recipe.EnvVersion)
toDeployVersion = app.Recipe.EnvVersion
}
if !internal.Chaos && toDeployVersion == "" {
if err := getLatestVersionOrCommit(app, &toDeployVersion); err != nil {
log.Fatal(err)
}
}
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)
}
if err := getChaosVersion(app, &toDeployVersion, &toDeployChaosVersion); err != nil {
log.Fatal(err)
}
}
isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil {
log.Fatal(err)
}
if isChaosCommit {
log.Debugf("assuming chaos commit: %s", toDeployVersion)
internal.Chaos = true
toDeployChaosVersion = toDeployVersion
toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
if 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)
@ -194,6 +156,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
@ -224,23 +187,22 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
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),
deployWarnMessages = append(deployWarnMessages,
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
)
}
}
if !internal.NoDomainChecks {
domainName, ok := app.Env["DOMAIN"]
if ok {
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 as no DOMAIN=... configured for app")
log.Debug("skipping domain checks, no DOMAIN=... configured")
}
} else {
log.Debug("skipping domain checks as requested")
log.Debug("skipping domain checks")
}
deployedVersion := config.NO_VERSION_DEFAULT
@ -248,13 +210,20 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
deployedVersion = deployMeta.Version
}
toWriteVersion := toDeployVersion
if internal.Chaos || isChaosCommit {
toWriteVersion = toDeployChaosVersion
}
if err := internal.DeployOverview(
app,
warnMessages,
deployWarnMessages,
deployedVersion,
deployMeta.ChaosVersion,
toDeployVersion,
toDeployChaosVersion); err != nil {
toDeployChaosVersion,
toWriteVersion,
); err != nil {
log.Fatal(err)
}
@ -262,7 +231,8 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
if err != nil {
log.Fatal(err)
}
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
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)
@ -276,30 +246,77 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
}
}
app.Recipe.Version = toDeployVersion
if toDeployChaosVersion != config.CHAOS_DEFAULT {
app.Recipe.Version = toDeployChaosVersion
}
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %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")
func getChaosVersion(app app.App, toDeployVersion, toDeployChaosVersion *string) error {
var err error
*toDeployChaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
return err
}
return true, nil
*toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
if err != nil {
return err
}
return nil
}
// getSpecifiedVersion retrieves the specific version if available
func getSpecifiedVersion(args []string) string {
if len(args) >= 2 {
return args[1]
func getLatestVersionOrCommit(app app.App, toDeployVersion *string) error {
versions, err := app.Recipe.Tags()
if err != nil {
return err
}
return ""
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 {
return err
}
return nil
}
head, err := app.Recipe.Head()
if err != nil {
return err
}
*toDeployVersion = formatter.SmallSHA(head.String())
return 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 init() {
@ -332,6 +349,6 @@ func init() {
"no-converge-checks",
"c",
false,
"do not wait for converge logic checks",
"disable converge logic checks",
)
}

View File

@ -1,57 +0,0 @@
package app
import (
"testing"
"coopcloud.tech/abra/cli/internal"
)
func TestGetSpecificVersion(t *testing.T) {
tests := []struct {
input []string
expectedOutput string
}{
// No specified version when command has one or less args
{[]string{}, ""},
{[]string{"arg0"}, ""},
// Second in arg (index-1) is the specified result when command has more than 1 args
{[]string{"arg0", "arg1"}, "arg1"},
{[]string{"arg0", "arg1", "arg2"}, "arg1"},
}
for _, test := range tests {
if test.expectedOutput != getSpecifiedVersion(test.input) {
t.Fatalf("result for %s should be %s", test.input, test.expectedOutput)
}
}
}
func TestValidateChaosXORVersion(t *testing.T) {
tests := []struct {
input []string
isChaos bool
expectedResult bool
}{
// Chaos = true, Specified Version absent
{[]string{}, true, true},
// Chaos = false, Specified Version absent
{[]string{}, false, true},
// Chaos = true, Specified Version present
{[]string{"arg0", "arg1"}, true, false},
// Chaos = false, Specified Version present
{[]string{"arg0", "arg1", "arg2"}, false, true},
}
for _, test := range tests {
internal.Chaos = test.isChaos
res, _ := validateChaosXORVersion(test.input)
if res != test.expectedResult {
t.Fatalf(
"When args are %s and Chaos mode is %t result needs to be %t",
test.input,
test.isChaos,
test.expectedResult,
)
}
}
}

View File

@ -34,7 +34,7 @@ var AppLabelsCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -212,7 +212,7 @@ var AppNewCommand = &cobra.Command{
}
if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
log.Fatalf("writing recipe version failed: %s", err)
}
},
}

View File

@ -36,7 +36,7 @@ var AppPsCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -48,7 +48,7 @@ Pass "--all-services/-a" to restart all services.`,
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(false, false); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -28,7 +28,7 @@ Some restore scenarios may require service / app restarts.`,
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -1,13 +1,14 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
@ -25,11 +26,18 @@ var AppRollbackCommand = &cobra.Command{
Short: "Roll an app back to a previous version",
Long: `This command rolls an app back to a previous version.
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]".
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".
A rollback can be destructive, please ensure you have a copy of your app data
beforehand.`,
It is possible to "--force/-f" an downgrade if you want to re-deploy a specific
version.
Only the deployed version is consulted when trying to determine what downgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.
A downgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`,
Example: ` # standard rollback
abra app rollback 1312.net
@ -55,26 +63,15 @@ beforehand.`,
}
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
var (
downgradeWarnMessages []string
chosenDowngrade string
availableDowngrades []string
)
app := internal.ValidateApp(args)
stackName := app.StackName()
var specificVersion string
if len(args) == 2 {
specificVersion = args[1]
}
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if err := lint.LintForErrors(app.Recipe); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
@ -83,15 +80,13 @@ beforehand.`,
log.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
deployMeta, err := ensureDeployed(cl, app)
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err)
}
versions, err := app.Recipe.Tags()
@ -99,84 +94,56 @@ beforehand.`,
log.Fatal(err)
}
var availableDowngrades []string
if deployMeta.Version == "unknown" {
// NOTE(d1): we've no idea what the live deployment version is, so every
// possible downgrade can be shown. it's up to the user to make the choice
if deployMeta.Version == config.UNKNOWN_DEFAULT {
availableDowngrades = versions
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
}
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
if len(args) == 2 && args[1] != "" {
chosenDowngrade = args[1]
if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
log.Fatal(err)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) {
log.Fatalf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
log.Fatalf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
availableDowngrades = append(availableDowngrades, specificVersion)
availableDowngrades = append(availableDowngrades, chosenDowngrade)
}
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
warnMessages = append(warnMessages, fmt.Sprintf("attempting to rollback a chaos deployment"))
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
if err != nil {
log.Fatal(err)
}
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
log.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
}
if parsedVersion.IsLessThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableDowngrades = append(availableDowngrades, version)
}
}
if len(availableDowngrades) == 0 && !internal.Force {
if !downgradeAvailable {
log.Info("no available downgrades")
return
}
}
var chosenDowngrade string
if len(availableDowngrades) > 0 {
if internal.Force || internal.NoInput || specificVersion != "" {
if internal.Force || internal.NoInput || chosenDowngrade != "" {
if len(availableDowngrades) > 0 {
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
log.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
} else {
msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
msg = fmt.Sprintf("please select a downgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableDowngrades),
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return
}
}
} else {
if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil {
log.Fatal(err)
}
}
if internal.Force &&
chosenDowngrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
chosenDowngrade = deployMeta.Version
}
if chosenDowngrade == "" {
log.Fatal("unknown deployed version, unable to downgrade")
}
log.Debugf("choosing %s as version to rollback", chosenDowngrade)
if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
log.Fatal(err)
}
@ -194,6 +161,7 @@ beforehand.`,
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
@ -221,12 +189,13 @@ beforehand.`,
// NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview(
app,
warnMessages,
downgradeWarnMessages,
"rollback",
deployMeta.Version,
chaosVersion,
chosenDowngrade,
""); err != nil {
"",
); err != nil {
log.Fatal(err)
}
@ -234,13 +203,100 @@ beforehand.`,
log.Fatal(err)
}
app.Recipe.Version = chosenDowngrade
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
}
},
}
// chooseDowngrade prompts the user to choose an downgrade interactively.
func chooseDowngrade(
availableDowngrades []string,
deployMeta stack.DeployMeta,
chosenDowngrade *string,
) error {
msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = fmt.Sprintf(
"please select a downgrade (version: %s, chaos: %s):",
deployMeta.Version,
chaosVersion,
)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableDowngrades),
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err
}
return nil
}
// validateDownpgradeVersionArg validates the specific version.
func validateDowngradeVersionArg(
specificVersion string,
app app.App,
deployMeta stack.DeployMeta,
) error {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
return nil
}
// ensureDowngradesAvailable ensures that there are available downgrades.
func ensureDowngradesAvailable(
versions []string,
availableDowngrades *[]string,
deployMeta stack.DeployMeta,
) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return false, err
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return false, err
}
if parsedVersion.IsLessThan(parsedDeployedVersion) &&
!(parsedVersion.Equals(parsedDeployedVersion)) {
*availableDowngrades = append(*availableDowngrades, version)
}
}
if len(*availableDowngrades) == 0 && !internal.Force {
return false, nil
}
return true, nil
}
func init() {
AppRollbackCommand.Flags().BoolVarP(
&internal.Force,
@ -262,6 +318,6 @@ func init() {
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"do not wait for converge logic checks",
"disable converge logic checks",
)
}

View File

@ -45,7 +45,7 @@ var AppSecretGenerateCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
@ -170,7 +170,7 @@ environment. Typically, you can let Abra generate them for you on app creation
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
@ -258,7 +258,7 @@ var AppSecretRmCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
@ -351,7 +351,7 @@ var AppSecretLsCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -30,7 +30,7 @@ var AppServicesCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -62,12 +62,13 @@ Passing "--prune/-p" does not remove those volumes.`,
if err := internal.UndeployOverview(
app,
deployMeta.Version,
chaosVersion); err != nil {
chaosVersion,
); err != nil {
log.Fatal(err)
}
rmOpts := stack.Remove{
Namespaces: []string{app.StackName()},
Namespaces: []string{stackName},
Detach: false,
}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
@ -81,7 +82,7 @@ Passing "--prune/-p" does not remove those volumes.`,
}
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
log.Fatalf("writing undeployed recipe version in env file: %s", err)
log.Fatalf("writing recipe version failed: %s", err)
}
},
}

View File

@ -5,16 +5,19 @@ import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
@ -24,11 +27,18 @@ var AppUpgradeCommand = &cobra.Command{
Short: "Upgrade an app",
Long: `Upgrade an app.
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[version]".
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".
It is possible to "--force/-f" an upgrade if you want to re-deploy a specific
version.
Only the deployed version is consulted when trying to determine what upgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.
An upgrade can be destructive, please ensure you have a copy of your app data
beforehand.`,
beforehand. See "abra app backup" for more.`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -49,22 +59,26 @@ beforehand.`,
}
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
var (
upgradeWarnMessages []string
chosenUpgrade string
availableUpgrades []string
upgradeReleaseNotes string
)
app := internal.ValidateApp(args)
stackName := app.StackName()
var specificVersion string
if len(args) == 2 {
specificVersion = args[1]
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
deployMeta, err := ensureDeployed(cl, app)
if err != nil {
log.Fatal(err)
}
@ -72,134 +86,69 @@ beforehand.`,
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)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
}
versions, err := app.Recipe.Tags()
if err != nil {
log.Fatal(err)
}
var availableUpgrades []string
if deployMeta.Version == "unknown" {
// NOTE(d1): we've no idea what the live deployment version is, so every
// possible upgrade can be shown. it's up to the user to make the choice
if deployMeta.Version == config.UNKNOWN_DEFAULT {
availableUpgrades = versions
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
}
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if len(args) == 2 && args[1] != "" {
chosenUpgrade = args[1]
if err := validateUpgradeVersionArg(chosenUpgrade, app, deployMeta); err != nil {
log.Fatal(err)
}
availableUpgrades = append(availableUpgrades, chosenUpgrade)
}
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" {
upgradeAvailable, err := ensureUpgradesAvailable(versions, &availableUpgrades, deployMeta)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
log.Fatal(err)
}
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) {
log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
availableUpgrades = append(availableUpgrades, specificVersion)
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
log.Fatal(err)
}
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
warnMessages = append(warnMessages, fmt.Sprintf("attempting to upgrade a chaos deployment"))
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableUpgrades = append(availableUpgrades, version)
}
}
if len(availableUpgrades) == 0 && !internal.Force {
if !upgradeAvailable {
log.Info("no available upgrades")
return
}
}
var chosenUpgrade string
if len(availableUpgrades) > 0 {
if internal.Force || internal.NoInput || specificVersion != "" {
if internal.Force || internal.NoInput || chosenUpgrade != "" {
if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
log.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else {
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
msg = fmt.Sprintf("please select an upgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableUpgrades),
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return
}
}
} else {
if err := chooseUpgrade(availableUpgrades, deployMeta, &chosenUpgrade); err != nil {
log.Fatal(err)
}
}
if internal.Force && chosenUpgrade == "" {
warnMessages = append(warnMessages, fmt.Sprintf("%s is already upgraded to latest", app.Name))
if internal.Force &&
chosenUpgrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
chosenUpgrade = deployMeta.Version
}
// if release notes written after git tag published, read them before we
// check out the tag and then they'll appear to be missing. this covers
// when we obviously will forget to write release notes before publishing
var releaseNotes string
if chosenUpgrade != "" {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
log.Fatal(err)
}
for _, version := range internal.SortVersionsDesc(versions) {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version)
if err != nil {
log.Fatal(err)
}
if note != "" {
releaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
if chosenUpgrade == "" {
log.Fatal("unknown deployed version, unable to upgrade")
}
log.Debugf("choosing %s as version to upgrade", chosenUpgrade)
// NOTE(d1): if release notes written after git tag published, read them
// before we check out the tag and then they'll appear to be missing. this
// covers when we obviously will forget to write release notes before
// publishing
if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
log.Fatal(err)
}
if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
log.Fatal(err)
}
@ -217,6 +166,7 @@ beforehand.`,
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
@ -243,30 +193,35 @@ beforehand.`,
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),
upgradeWarnMessages = append(upgradeWarnMessages,
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
)
}
}
if showReleaseNotes {
fmt.Print(releaseNotes)
fmt.Print(upgradeReleaseNotes)
return
}
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
if deployMeta.ChaosVersion == "" {
chaosVersion = config.UNKNOWN_DEFAULT
}
}
if err := internal.NewVersionOverview(
app,
warnMessages,
upgradeWarnMessages,
"upgrade",
deployMeta.Version,
chaosVersion,
chosenUpgrade,
releaseNotes); err != nil {
upgradeReleaseNotes,
); err != nil {
log.Fatal(err)
}
@ -274,7 +229,8 @@ beforehand.`,
if err != nil {
log.Fatal(err)
}
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
log.Fatal(err)
@ -283,18 +239,162 @@ beforehand.`,
postDeployCmds, ok := app.Env["POST_UPGRADE_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 = chosenUpgrade
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
}
},
}
// chooseUpgrade prompts the user to choose an upgrade interactively.
func chooseUpgrade(
availableUpgrades []string,
deployMeta stack.DeployMeta,
chosenUpgrade *string,
) error {
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = fmt.Sprintf(
"please select an upgrade (version: %s, chaos: %s):",
deployMeta.Version,
chaosVersion,
)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableUpgrades),
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err
}
return nil
}
func getReleaseNotes(
app app.App,
versions []string,
chosenUpgrade string,
deployMeta stack.DeployMeta,
upgradeReleaseNotes *string,
) error {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
return err
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return err
}
for _, version := range internal.SortVersionsDesc(versions) {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return err
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version)
if err != nil {
return err
}
if note != "" {
*upgradeReleaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
return nil
}
// ensureUpgradesAvailable ensures that there are available upgrades.
func ensureUpgradesAvailable(
versions []string,
availableUpgrades *[]string,
deployMeta stack.DeployMeta,
) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return false, err
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return false, err
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
!(parsedVersion.Equals(parsedDeployedVersion)) {
*availableUpgrades = append(*availableUpgrades, version)
}
}
if len(*availableUpgrades) == 0 && !internal.Force {
return false, nil
}
return true, nil
}
// validateUpgradeVersionArg validates the specific version.
func validateUpgradeVersionArg(
specificVersion string,
app app.App,
deployMeta stack.DeployMeta,
) error {
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return err
}
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
return nil
}
// ensureDeployed ensures the app is deployed and if so, returns deployment
// meta info.
func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) {
log.Debugf("checking whether %s is already deployed", app.StackName())
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
return stack.DeployMeta{}, err
}
if !deployMeta.IsDeployed {
return stack.DeployMeta{}, fmt.Errorf("%s is not deployed?", app.Name)
}
return deployMeta, nil
}
var (
showReleaseNotes bool
)
@ -320,7 +420,7 @@ func init() {
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"do not wait for converge logic checks",
"disable converge logic checks",
)
AppUpgradeCommand.Flags().BoolVarP(

View File

@ -2,9 +2,10 @@ package internal
var (
// NOTE(d1): global
Debug bool
NoInput bool
Offline bool
Debug bool
NoInput bool
Offline bool
IgnoreEnvVersion bool
// NOTE(d1): sub-command specific
Chaos bool

View File

@ -68,11 +68,17 @@ func NewVersionOverview(
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", deployedVersion},
{"CHAOS ", deployedChaosVersion},
{upperKind, "---"},
{"VERSION", toDeployVersion},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Domain)), "---"},
{"OLD", app.Recipe.EnvVersion},
{"NEW", toDeployVersion},
}
overview := formatter.CreateOverview(
@ -119,7 +125,9 @@ func DeployOverview(
deployedVersion string,
deployedChaosVersion string,
toDeployVersion,
toDeployChaosVersion string) error {
toDeployChaosVersion string,
toWriteVersion string,
) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles
@ -154,6 +162,10 @@ func DeployOverview(
{"NEW DEPLOYMENT", "---"},
{"VERSION", toDeployVersion},
{"CHAOS", toDeployChaosVersion},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"},
{"CURRENT VERSION", app.Recipe.EnvVersion},
{"NEW VERSION", toWriteVersion},
}
overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows)
@ -210,9 +222,14 @@ func UndeployOverview(
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"DEPLOYED", version},
{"CHAOS", chaosVersion},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"},
{"CURRENT VERSION", app.Recipe.EnvVersion},
{"NEW VERSION", version},
}
overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows)

11
cli/internal/ensure.go Normal file
View File

@ -0,0 +1,11 @@
package internal
import "coopcloud.tech/abra/pkg/recipe"
func GetEnsureContext() recipe.EnsureContext {
return recipe.EnsureContext{
Chaos,
Offline,
IgnoreEnvVersion,
}
}

View File

@ -1,18 +0,0 @@
package internal
import (
"os"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
)
// ShowSubcommandHelpAndError exits the program on error, logs the error to the
// terminal, and shows the help command.
func ShowSubcommandHelpAndError(cmd *cli.Command, err interface{}) {
if err2 := cli.ShowSubcommandHelp(cmd); err2 != nil {
log.Error(err2)
}
log.Error(err)
os.Exit(1)
}

View File

@ -34,9 +34,10 @@ var RecipeFetchCommand = &cobra.Command{
log.Fatal("cannot use [recipe] and --all/-a together")
}
ensureCtx := internal.GetEnsureContext()
if recipeName != "" {
r := internal.ValidateRecipe(args, cmd.Name())
if err := r.Ensure(false, false); err != nil {
if err := r.Ensure(ensureCtx); err != nil {
log.Fatal(err)
}
return
@ -50,7 +51,7 @@ var RecipeFetchCommand = &cobra.Command{
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
for recipeName := range catalogue {
r := recipe.Get(recipeName)
if err := r.Ensure(false, false); err != nil {
if err := r.Ensure(ensureCtx); err != nil {
log.Error(err)
}
catlBar.Add(1)

View File

@ -23,7 +23,7 @@ var RecipeLintCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -63,7 +63,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}

View File

@ -95,20 +95,37 @@ func Run(version, commit string) {
}
rootCmd.PersistentFlags().BoolVarP(
&internal.Debug, "debug", "d", false,
&internal.Debug,
"debug",
"d",
false,
"show debug messages",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput, "no-input", "n", false,
&internal.NoInput,
"no-input",
"n",
false,
"toggle non-interactive mode",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.Offline, "offline", "o", false,
&internal.Offline,
"offline",
"o",
false,
"prefer offline & filesystem access",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.IgnoreEnvVersion,
"ignore-env-version",
"i",
false,
"ignore .env version checkout",
)
catalogue.CatalogueCommand.AddCommand(
catalogue.CatalogueGenerateCommand,
)