forked from toolshed/abra
@ -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(
|
||||
|
Reference in New Issue
Block a user