From 93d07c862ddf30dd92729572efea5237c5521a4c Mon Sep 17 00:00:00 2001 From: Taylor Thomas Date: Mon, 8 Jul 2019 13:36:34 -0600 Subject: [PATCH] feat(*): Adds back --atomic functionality to Helm 3 This does not include the cleanup on fail logic as that will be reintroduced in a future PR Signed-off-by: Taylor Thomas --- cmd/helm/install.go | 1 + cmd/helm/upgrade.go | 2 ++ pkg/action/install.go | 38 +++++++++++++++------- pkg/action/install_test.go | 8 +++++ pkg/action/upgrade.go | 66 +++++++++++++++++++++++++++++++------- 5 files changed, 92 insertions(+), 23 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 40e810c4a..ca2ef037f 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -128,6 +128,7 @@ func addInstallFlags(f *pflag.FlagSet, client *action.Install) { f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "run helm dependency update before installing the chart") + f.BoolVar(&client.Atomic, "atomic", false, "if set, installation process purges chart on fail. The --wait flag will be set automatically if --atomic is used") addValueOptionsFlags(f, &client.ValueOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions) } diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 4bbf46d4f..38c36485f 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -96,6 +96,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Wait = client.Wait instClient.Devel = client.Devel instClient.Namespace = client.Namespace + instClient.Atomic = client.Atomic _, err := runInstall(args, instClient, out) return err @@ -147,6 +148,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.ResetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") f.BoolVar(&client.ReuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored.") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.BoolVar(&client.Atomic, "atomic", false, "if set, upgrade process rolls back changes made in case of failed upgrade. The --wait flag will be set automatically if --atomic is used") f.IntVar(&client.MaxHistory, "history-max", 0, "limit the maximum number of revisions saved per release. Use 0 for no limit.") addChartPathOptionsFlags(f, &client.ChartPathOptions) addValueOptionsFlags(f, &client.ValueOptions) diff --git a/pkg/action/install.go b/pkg/action/install.go index 22cd5e5af..c834eea82 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -82,6 +82,7 @@ type Install struct { GenerateName bool NameTemplate string OutputDir string + Atomic bool } type ValueOptions struct { @@ -118,6 +119,10 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) { return nil, err } + // Make sure if Atomic is set, that wait is set as well. This makes it so + // the user doesn't have to specify both + i.Wait = i.Wait || i.Atomic + caps, err := i.cfg.getCapabilities() if err != nil { return nil, err @@ -178,9 +183,7 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) { // pre-install hooks if !i.DisableHooks { if err := i.execHook(rel.Hooks, hooks.PreInstall); err != nil { - rel.SetStatus(release.StatusFailed, "failed pre-install: "+err.Error()) - _ = i.replaceRelease(rel) - return rel, err + return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err)) } } @@ -189,26 +192,20 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) { // to true, since that is basically an upgrade operation. buf := bytes.NewBufferString(rel.Manifest) if err := i.cfg.KubeClient.Create(buf); err != nil { - rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) - i.recordRelease(rel) // Ignore the error, since we have another error to deal with. - return rel, errors.Wrapf(err, "release %s failed", i.ReleaseName) + return i.failRelease(rel, err) } if i.Wait { buf := bytes.NewBufferString(rel.Manifest) if err := i.cfg.KubeClient.Wait(buf, i.Timeout); err != nil { - rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) - i.recordRelease(rel) // Ignore the error, since we have another error to deal with. - return rel, errors.Wrapf(err, "release %s failed", i.ReleaseName) + return i.failRelease(rel, err) } } if !i.DisableHooks { if err := i.execHook(rel.Hooks, hooks.PostInstall); err != nil { - rel.SetStatus(release.StatusFailed, "failed post-install: "+err.Error()) - _ = i.replaceRelease(rel) - return rel, err + return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err)) } } @@ -226,6 +223,23 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) { return rel, nil } +func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { + rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) + if i.Atomic { + i.cfg.Log("Install failed and atomic is set, purging release") + uninstall := NewUninstall(i.cfg) + uninstall.DisableHooks = i.DisableHooks + uninstall.KeepHistory = false + uninstall.Timeout = i.Timeout + if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil { + return rel, errors.Wrapf(uninstallErr, "an error occurred while purging the release. original install error: %s", err) + } + return rel, errors.Wrapf(err, "release %s failed, and has been purged due to atomic being set", i.ReleaseName) + } + i.recordRelease(rel) // Ignore the error, since we have another error to deal with. + return rel, err +} + // availableName tests whether a name is available // // Roughly, this will return an error if name is diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 12f9bb3b2..fc5d171dc 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -235,6 +235,14 @@ func TestInstallRelease_KubeVersion(t *testing.T) { is.Contains(err.Error(), "chart requires kubernetesVersion") } +func TestInstallRelease_Wait(t *testing.T) { + t.Fail("Implement me") +} + +func TestInstallRelease_Atomic(t *testing.T) { + t.Fail("Implement me") +} + func TestNameTemplate(t *testing.T) { testCases := []nameTemplateTestCase{ // Just a straight up nop please diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 6c2c67a02..ebf889d77 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -29,6 +29,7 @@ import ( "helm.sh/helm/pkg/hooks" "helm.sh/helm/pkg/kube" "helm.sh/helm/pkg/release" + "helm.sh/helm/pkg/releaseutil" ) // Upgrade is the action for upgrading releases. @@ -54,6 +55,7 @@ type Upgrade struct { Recreate bool // MaxHistory limits the maximum number of revisions saved per release MaxHistory int + Atomic bool } // NewUpgrade creates a new Upgrade object with the given configuration. @@ -69,6 +71,10 @@ func (u *Upgrade) Run(name string, chart *chart.Chart) (*release.Release, error) return nil, err } + // Make sure if Atomic is set, that wait is set as well. This makes it so + // the user doesn't have to specify both + u.Wait = u.Wait || u.Atomic + if err := validateReleaseName(name); err != nil { return nil, errors.Errorf("upgradeRelease: Release name is invalid: %s", name) } @@ -196,35 +202,28 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea // pre-upgrade hooks if !u.DisableHooks { if err := u.execHook(upgradedRelease.Hooks, hooks.PreUpgrade); err != nil { - return upgradedRelease, err + return u.failRelease(upgradedRelease, fmt.Errorf("pre-upgrade hooks failed: %s", err)) } } else { u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name) } if err := u.upgradeRelease(originalRelease, upgradedRelease); err != nil { - msg := fmt.Sprintf("Upgrade %q failed: %s", upgradedRelease.Name, err) - u.cfg.Log("warning: %s", msg) - upgradedRelease.Info.Status = release.StatusFailed - upgradedRelease.Info.Description = msg u.cfg.recordRelease(originalRelease) - u.cfg.recordRelease(upgradedRelease) - return upgradedRelease, err + return u.failRelease(upgradedRelease, err) } if u.Wait { buf := bytes.NewBufferString(upgradedRelease.Manifest) if err := u.cfg.KubeClient.Wait(buf, u.Timeout); err != nil { - upgradedRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", upgradedRelease.Name, err.Error())) u.cfg.recordRelease(originalRelease) - u.cfg.recordRelease(upgradedRelease) - return upgradedRelease, errors.Wrapf(err, "release %s failed", upgradedRelease.Name) + return u.failRelease(upgradedRelease, err) } } // post-upgrade hooks if !u.DisableHooks { if err := u.execHook(upgradedRelease.Hooks, hooks.PostUpgrade); err != nil { - return upgradedRelease, err + return u.failRelease(upgradedRelease, fmt.Errorf("post-upgrade hooks failed: %s", err)) } } @@ -237,6 +236,51 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea return upgradedRelease, nil } +func (u *Upgrade) failRelease(rel *release.Release, err error) (*release.Release, error) { + msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err) + u.cfg.Log("warning: %s", msg) + + rel.Info.Status = release.StatusFailed + rel.Info.Description = msg + u.cfg.recordRelease(rel) + if u.Atomic { + u.cfg.Log("Upgrade failed and atomic is set, rolling back to last successful release") + + // As a protection, get the last successful release before rollback. + // If there are no successful releases, bail out + hist := NewHistory(u.cfg) + fullHistory, herr := hist.Run(rel.Name) + if herr != nil { + return rel, errors.Wrapf(herr, "an error occurred while finding last successful release. original upgrade error: %s", err) + } + + // There isn't a way to tell if a previous release was successful, but + // generally failed releases do not get superseded unless the next + // release is successful, so this should be relatively safe + filteredHistory := releaseutil.StatusFilter(release.StatusSuperseded).Filter(fullHistory) + if len(filteredHistory) == 0 { + return rel, errors.Wrap(err, "unable to find a previously successful release when attempting to rollback. original upgrade error") + } + + releaseutil.Reverse(filteredHistory, releaseutil.SortByRevision) + + rollin := NewRollback(u.cfg) + rollin.Version = filteredHistory[0].Version + rollin.Wait = u.Wait + rollin.DisableHooks = u.DisableHooks + rollin.Recreate = u.Recreate + rollin.Force = u.Force + rollin.Timeout = u.Timeout + + if _, rollErr := rollin.Run(rel.Name); err != nil { + return rel, errors.Wrapf(rollErr, "an error occurred while rolling back the release. original upgrade error: %s", err) + } + return rel, errors.Wrapf(err, "release %s failed, and has been rolled back due to atomic being set", rel.Name) + } + + return rel, err +} + // upgradeRelease performs an upgrade from current to target release func (u *Upgrade) upgradeRelease(current, target *release.Release) error { cm := bytes.NewBufferString(current.Manifest)