forked from toolshed/abra
		
	
		
			
				
	
	
		
			344 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			344 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package app
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 
 | |
| 	"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/i18n"
 | |
| 	"coopcloud.tech/abra/pkg/lint"
 | |
| 	stack "coopcloud.tech/abra/pkg/upstream/stack"
 | |
| 	"coopcloud.tech/tagcmp"
 | |
| 
 | |
| 	"coopcloud.tech/abra/cli/internal"
 | |
| 	"coopcloud.tech/abra/pkg/client"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	"github.com/AlecAivazis/survey/v2"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| var AppRollbackCommand = &cobra.Command{
 | |
| 	// translators: `app rollback` command
 | |
| 	Use:     i18n.G("rollback <domain> [version] [flags]"),
 | |
| 	Aliases: []string{i18n.G("rl")},
 | |
| 	Short:   i18n.G("Roll an app back to a previous version"),
 | |
| 	Long: i18n.G(`This command rolls an app back to a previous 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 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: i18n.G(` # standard rollback
 | |
|   abra app rollback 1312.net
 | |
| 
 | |
|   # rollback to specific version
 | |
|   abra app rollback 1312.net 2.0.0+1.2.3`),
 | |
| 	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 {
 | |
| 				return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
 | |
| 			}
 | |
| 			return autocomplete.RecipeVersionComplete(app.Recipe.Name)
 | |
| 		default:
 | |
| 			return nil, cobra.ShellCompDirectiveError
 | |
| 		}
 | |
| 	},
 | |
| 	Run: func(cmd *cobra.Command, args []string) {
 | |
| 		var (
 | |
| 			downgradeWarnMessages []string
 | |
| 			chosenDowngrade       string
 | |
| 			availableDowngrades   []string
 | |
| 		)
 | |
| 
 | |
| 		app := internal.ValidateApp(args)
 | |
| 
 | |
| 		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		cl, err := client.New(app.Server)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		deployMeta, err := ensureDeployed(cl, app)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if err := lint.LintForErrors(app.Recipe); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		versions, err := app.Recipe.Tags()
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		// 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
 | |
| 		}
 | |
| 
 | |
| 		if len(args) == 2 && args[1] != "" {
 | |
| 			chosenDowngrade = args[1]
 | |
| 
 | |
| 			if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			availableDowngrades = append(availableDowngrades, chosenDowngrade)
 | |
| 		}
 | |
| 
 | |
| 		if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
 | |
| 			downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
 | |
| 			if err != nil {
 | |
| 				log.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			if !downgradeAvailable {
 | |
| 				log.Info(i18n.G("no available downgrades"))
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if internal.Force || internal.NoInput || chosenDowngrade != "" {
 | |
| 			if len(availableDowngrades) > 0 {
 | |
| 				chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
 | |
| 			}
 | |
| 		} 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(i18n.G("unknown deployed version, unable to downgrade"))
 | |
| 		}
 | |
| 
 | |
| 		log.Debug(i18n.G("choosing %s as version to rollback", chosenDowngrade))
 | |
| 
 | |
| 		if _, err := app.Recipe.EnsureVersion(chosenDowngrade); 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, chosenDowngrade)
 | |
| 		}
 | |
| 		appPkg.SetUpdateLabel(compose, stackName, app.Env)
 | |
| 
 | |
| 		// NOTE(d1): no release notes implemeneted for rolling back
 | |
| 		if err := internal.DeployOverview(
 | |
| 			app,
 | |
| 			deployMeta.Version,
 | |
| 			chosenDowngrade,
 | |
| 			"",
 | |
| 			downgradeWarnMessages,
 | |
| 		); 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,
 | |
| 			stackName,
 | |
| 			app.Server,
 | |
| 			internal.DontWaitConverge,
 | |
| 			f,
 | |
| 		); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
 | |
| 			log.Fatal(i18n.G("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 := i18n.G("please select a downgrade (version: %s):", deployMeta.Version)
 | |
| 
 | |
| 	if deployMeta.IsChaos {
 | |
| 		chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
 | |
| 
 | |
| 		msg = i18n.G(
 | |
| 			"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 errors.New(i18n.G("current deployment '%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name))
 | |
| 	}
 | |
| 
 | |
| 	parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
 | |
| 	if err != nil {
 | |
| 		return errors.New(i18n.G("'%s' is not a known version for %s", specificVersion, app.Recipe.Name))
 | |
| 	}
 | |
| 
 | |
| 	if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) &&
 | |
| 		!parsedSpecificVersion.Equals(parsedDeployedVersion) {
 | |
| 		return errors.New(i18n.G("%s is not a downgrade for %s?", deployMeta.Version, specificVersion))
 | |
| 	}
 | |
| 
 | |
| 	if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
 | |
| 		return errors.New(i18n.G("%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,
 | |
| 		i18n.G("force"),
 | |
| 		i18n.G("f"),
 | |
| 		false,
 | |
| 		i18n.G("perform action without further prompt"),
 | |
| 	)
 | |
| 
 | |
| 	AppRollbackCommand.Flags().BoolVarP(
 | |
| 		&internal.NoDomainChecks,
 | |
| 		i18n.G("no-domain-checks"),
 | |
| 		i18n.G("D"),
 | |
| 		false,
 | |
| 		i18n.G("disable public DNS checks"),
 | |
| 	)
 | |
| 
 | |
| 	AppRollbackCommand.Flags().BoolVarP(
 | |
| 		&internal.DontWaitConverge,
 | |
| 		i18n.G("no-converge-checks"),
 | |
| 		i18n.G("c"),
 | |
| 		false,
 | |
| 		i18n.G("disable converge logic checks"),
 | |
| 	)
 | |
| }
 |