forked from toolshed/abra
		
	
		
			
				
	
	
		
			350 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			350 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package app
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"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"
 | 
						|
)
 | 
						|
 | 
						|
// translators: `abra app rollback` aliases. use a comma separated list of
 | 
						|
// aliases with no spaces in between
 | 
						|
var appRollbackAliases = i18n.G("rl")
 | 
						|
 | 
						|
var AppRollbackCommand = &cobra.Command{
 | 
						|
	// translators: `app rollback` command
 | 
						|
	Use:     i18n.G("rollback <domain> [version] [flags]"),
 | 
						|
	Aliases: strings.Split(appRollbackAliases, ","),
 | 
						|
	// translators: Short description for `app rollback` command
 | 
						|
	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"),
 | 
						|
	)
 | 
						|
}
 |