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 <taylor.thomas@microsoft.com>
pull/6011/head
Taylor Thomas 5 years ago
parent ff818fceca
commit 93d07c862d

@ -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)
}

@ -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)

@ -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

@ -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

@ -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)

Loading…
Cancel
Save