Merge pull request #6011 from thomastaylor312/feat/atomic

feat(*) Adds atomic flag to v3
pull/6059/head
Taylor Thomas 5 years ago committed by GitHub
commit 2c397b6879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

469
Gopkg.lock generated

File diff suppressed because it is too large Load Diff

@ -113,11 +113,11 @@ $(GOIMPORTS):
# install vendored dependencies # install vendored dependencies
vendor: Gopkg.lock vendor: Gopkg.lock
$(DEP) ensure -v --vendor-only $(DEP) ensure --vendor-only
# update vendored dependencies # update vendored dependencies
Gopkg.lock: Gopkg.toml Gopkg.lock: Gopkg.toml
$(DEP) ensure -v --no-vendor $(DEP) ensure --no-vendor
Gopkg.toml: $(DEP) Gopkg.toml: $(DEP)

@ -31,7 +31,7 @@ import (
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/helmpath" "helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/kube" kubefake "helm.sh/helm/pkg/kube/fake"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/repo" "helm.sh/helm/pkg/repo"
"helm.sh/helm/pkg/storage" "helm.sh/helm/pkg/storage"
@ -116,7 +116,7 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command,
actionConfig := &action.Configuration{ actionConfig := &action.Configuration{
Releases: store, Releases: store,
KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard}, KubeClient: &kubefake.PrintingKubeClient{Out: ioutil.Discard},
Capabilities: chartutil.DefaultCapabilities, Capabilities: chartutil.DefaultCapabilities,
Log: func(format string, v ...interface{}) {}, Log: func(format string, v ...interface{}) {},
} }

@ -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.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.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.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) addValueOptionsFlags(f, &client.ValueOptions)
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
} }

@ -28,7 +28,7 @@ import (
"helm.sh/helm/cmd/helm/require" "helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/kube" kubefake "helm.sh/helm/pkg/kube/fake"
"helm.sh/helm/pkg/storage" "helm.sh/helm/pkg/storage"
"helm.sh/helm/pkg/storage/driver" "helm.sh/helm/pkg/storage/driver"
) )
@ -46,7 +46,7 @@ func newTemplateCmd(out io.Writer) *cobra.Command {
customConfig := &action.Configuration{ customConfig := &action.Configuration{
// Add mock objects in here so it doesn't use Kube API server // Add mock objects in here so it doesn't use Kube API server
Releases: storage.Init(driver.NewMemory()), Releases: storage.Init(driver.NewMemory()),
KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard}, KubeClient: &kubefake.PrintingKubeClient{Out: ioutil.Discard},
Capabilities: chartutil.DefaultCapabilities, Capabilities: chartutil.DefaultCapabilities,
Log: func(format string, v ...interface{}) { Log: func(format string, v ...interface{}) {
fmt.Fprintf(out, format, v...) fmt.Fprintf(out, format, v...)

@ -96,6 +96,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.Wait = client.Wait instClient.Wait = client.Wait
instClient.Devel = client.Devel instClient.Devel = client.Devel
instClient.Namespace = client.Namespace instClient.Namespace = client.Namespace
instClient.Atomic = client.Atomic
_, err := runInstall(args, instClient, out) _, err := runInstall(args, instClient, out)
return err 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.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.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.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.") 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) addChartPathOptionsFlags(f, &client.ChartPathOptions)
addValueOptionsFlags(f, &client.ValueOptions) addValueOptionsFlags(f, &client.ValueOptions)

@ -17,17 +17,15 @@ package action
import ( import (
"flag" "flag"
"io"
"io/ioutil" "io/ioutil"
"testing" "testing"
"time" "time"
"github.com/pkg/errors"
fakeclientset "k8s.io/client-go/kubernetes/fake" fakeclientset "k8s.io/client-go/kubernetes/fake"
"helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart"
"helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/kube" kubefake "helm.sh/helm/pkg/kube/fake"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/storage" "helm.sh/helm/pkg/storage"
"helm.sh/helm/pkg/storage/driver" "helm.sh/helm/pkg/storage/driver"
@ -40,7 +38,7 @@ func actionConfigFixture(t *testing.T) *Configuration {
return &Configuration{ return &Configuration{
Releases: storage.Init(driver.NewMemory()), Releases: storage.Init(driver.NewMemory()),
KubeClient: &kube.PrintingKubeClient{Out: ioutil.Discard}, KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: ioutil.Discard}},
Capabilities: chartutil.DefaultCapabilities, Capabilities: chartutil.DefaultCapabilities,
Log: func(format string, v ...interface{}) { Log: func(format string, v ...interface{}) {
t.Helper() t.Helper()
@ -55,7 +53,7 @@ var manifestWithHook = `kind: ConfigMap
metadata: metadata:
name: test-cm name: test-cm
annotations: annotations:
"helm.sh/hook": post-install,pre-delete "helm.sh/hook": post-install,pre-delete,post-upgrade
data: data:
name: value` name: value`
@ -210,20 +208,6 @@ func namedReleaseStub(name string, status release.Status) *release.Release {
} }
} }
func newHookFailingKubeClient() *hookFailingKubeClient {
return &hookFailingKubeClient{
PrintingKubeClient: kube.PrintingKubeClient{Out: ioutil.Discard},
}
}
type hookFailingKubeClient struct {
kube.PrintingKubeClient
}
func (h *hookFailingKubeClient) WatchUntilReady(r io.Reader, timeout time.Duration) error {
return errors.New("Failed watch")
}
func TestGetVersionSet(t *testing.T) { func TestGetVersionSet(t *testing.T) {
client := fakeclientset.NewSimpleClientset() client := fakeclientset.NewSimpleClientset()

@ -82,6 +82,7 @@ type Install struct {
GenerateName bool GenerateName bool
NameTemplate string NameTemplate string
OutputDir string OutputDir string
Atomic bool
} }
type ValueOptions struct { type ValueOptions struct {
@ -118,6 +119,10 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) {
return nil, err 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() caps, err := i.cfg.getCapabilities()
if err != nil { if err != nil {
return nil, err return nil, err
@ -178,9 +183,7 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) {
// pre-install hooks // pre-install hooks
if !i.DisableHooks { if !i.DisableHooks {
if err := i.execHook(rel.Hooks, hooks.PreInstall); err != nil { if err := i.execHook(rel.Hooks, hooks.PreInstall); err != nil {
rel.SetStatus(release.StatusFailed, "failed pre-install: "+err.Error()) return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
_ = i.replaceRelease(rel)
return rel, err
} }
} }
@ -189,26 +192,20 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) {
// to true, since that is basically an upgrade operation. // to true, since that is basically an upgrade operation.
buf := bytes.NewBufferString(rel.Manifest) buf := bytes.NewBufferString(rel.Manifest)
if err := i.cfg.KubeClient.Create(buf); err != nil { if err := i.cfg.KubeClient.Create(buf); err != nil {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) return i.failRelease(rel, err)
i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
return rel, errors.Wrapf(err, "release %s failed", i.ReleaseName)
} }
if i.Wait { if i.Wait {
buf := bytes.NewBufferString(rel.Manifest) buf := bytes.NewBufferString(rel.Manifest)
if err := i.cfg.KubeClient.Wait(buf, i.Timeout); err != nil { 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())) return i.failRelease(rel, err)
i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
return rel, errors.Wrapf(err, "release %s failed", i.ReleaseName)
} }
} }
if !i.DisableHooks { if !i.DisableHooks {
if err := i.execHook(rel.Hooks, hooks.PostInstall); err != nil { if err := i.execHook(rel.Hooks, hooks.PostInstall); err != nil {
rel.SetStatus(release.StatusFailed, "failed post-install: "+err.Error()) return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
_ = i.replaceRelease(rel)
return rel, err
} }
} }
@ -226,6 +223,23 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) {
return rel, nil 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, uninstalling 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 uninstalling the release. original install error: %s", err)
}
return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled 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 // availableName tests whether a name is available
// //
// Roughly, this will return an error if name is // Roughly, this will return an error if name is

@ -30,6 +30,8 @@ import (
"helm.sh/helm/internal/test" "helm.sh/helm/internal/test"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/storage/driver"
kubefake "helm.sh/helm/pkg/kube/fake"
) )
type nameTemplateTestCase struct { type nameTemplateTestCase struct {
@ -189,7 +191,9 @@ func TestInstallRelease_FailedHooks(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.ReleaseName = "failed-hooks" instAction.ReleaseName = "failed-hooks"
instAction.cfg.KubeClient = newHookFailingKubeClient() failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WatchUntilReadyError = fmt.Errorf("Failed watch")
instAction.cfg.KubeClient = failer
instAction.rawValues = map[string]interface{}{} instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart()) res, err := instAction.Run(buildChart())
@ -236,6 +240,63 @@ func TestInstallRelease_KubeVersion(t *testing.T) {
is.Contains(err.Error(), "chart requires kubernetesVersion") is.Contains(err.Error(), "chart requires kubernetesVersion")
} }
func TestInstallRelease_Wait(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "come-fail-away"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
instAction.cfg.KubeClient = failer
instAction.Wait = true
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
is.Error(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestInstallRelease_Atomic(t *testing.T) {
is := assert.New(t)
t.Run("atomic uninstall succeeds", func(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = "come-fail-away"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
instAction.cfg.KubeClient = failer
instAction.Atomic = true
instAction.rawValues = map[string]interface{}{}
res, err := instAction.Run(buildChart())
is.Error(err)
is.Contains(err.Error(), "I timed out")
is.Contains(err.Error(), "atomic")
// Now make sure it isn't in storage any more
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Equal(err, driver.ErrReleaseNotFound)
})
t.Run("atomic uninstall fails", func(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = "come-fail-away-with-me"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
failer.DeleteError = fmt.Errorf("uninstall fail")
instAction.cfg.KubeClient = failer
instAction.Atomic = true
instAction.rawValues = map[string]interface{}{}
_, err := instAction.Run(buildChart())
is.Error(err)
is.Contains(err.Error(), "I timed out")
is.Contains(err.Error(), "uninstall fail")
is.Contains(err.Error(), "an error occurred while uninstalling the release")
})
}
func TestNameTemplate(t *testing.T) { func TestNameTemplate(t *testing.T) {
testCases := []nameTemplateTestCase{ testCases := []nameTemplateTestCase{
// Just a straight up nop please // Just a straight up nop please

@ -1,3 +1,19 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action package action
import ( import (

@ -122,7 +122,16 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
if !u.KeepHistory { if !u.KeepHistory {
u.cfg.Log("purge requested for %s", name) u.cfg.Log("purge requested for %s", name)
err := u.purgeReleases(rels...) err := u.purgeReleases(rels...)
return res, errors.Wrap(err, "uninstall: Failed to purge the release") if err != nil {
errs = append(errs, errors.Wrap(err, "uninstall: Failed to purge the release"))
}
// Return the errors that occurred while deleting the release, if any
if len(errs) > 0 {
return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs))
}
return res, nil
} }
if err := u.cfg.Releases.Update(rel); err != nil { if err := u.cfg.Releases.Update(rel); err != nil {

@ -29,6 +29,7 @@ import (
"helm.sh/helm/pkg/hooks" "helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/kube" "helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil"
) )
// Upgrade is the action for upgrading releases. // Upgrade is the action for upgrading releases.
@ -54,6 +55,7 @@ type Upgrade struct {
Recreate bool Recreate bool
// MaxHistory limits the maximum number of revisions saved per release // MaxHistory limits the maximum number of revisions saved per release
MaxHistory int MaxHistory int
Atomic bool
} }
// NewUpgrade creates a new Upgrade object with the given configuration. // 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 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 { if err := validateReleaseName(name); err != nil {
return nil, errors.Errorf("upgradeRelease: Release name is invalid: %s", name) 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 // pre-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
if err := u.execHook(upgradedRelease.Hooks, hooks.PreUpgrade); err != nil { 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 { } else {
u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name) u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name)
} }
if err := u.upgradeRelease(originalRelease, upgradedRelease); err != nil { 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(originalRelease)
u.cfg.recordRelease(upgradedRelease) return u.failRelease(upgradedRelease, err)
return upgradedRelease, err
} }
if u.Wait { if u.Wait {
buf := bytes.NewBufferString(upgradedRelease.Manifest) buf := bytes.NewBufferString(upgradedRelease.Manifest)
if err := u.cfg.KubeClient.Wait(buf, u.Timeout); err != nil { 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(originalRelease)
u.cfg.recordRelease(upgradedRelease) return u.failRelease(upgradedRelease, err)
return upgradedRelease, errors.Wrapf(err, "release %s failed", upgradedRelease.Name)
} }
} }
// post-upgrade hooks // post-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
if err := u.execHook(upgradedRelease.Hooks, hooks.PostUpgrade); err != nil { 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,52 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
return upgradedRelease, nil 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.FilterFunc(func(r *release.Release) bool {
return r.Info.Status == release.StatusSuperseded || r.Info.Status == release.StatusDeployed
}).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 = true
rollin.DisableHooks = u.DisableHooks
rollin.Recreate = u.Recreate
rollin.Force = u.Force
rollin.Timeout = u.Timeout
if _, rollErr := rollin.Run(rel.Name); rollErr != 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 // upgradeRelease performs an upgrade from current to target release
func (u *Upgrade) upgradeRelease(current, target *release.Release) error { func (u *Upgrade) upgradeRelease(current, target *release.Release) error {
cm := bytes.NewBufferString(current.Manifest) cm := bytes.NewBufferString(current.Manifest)
@ -303,7 +348,6 @@ func (u *Upgrade) execHook(hs []*release.Hook, hook string) error {
} }
sort.Sort(hookByWeight(executingHooks)) sort.Sort(hookByWeight(executingHooks))
for _, h := range executingHooks { for _, h := range executingHooks {
if err := deleteHookByPolicy(u.cfg, h, hooks.BeforeHookCreation); err != nil { if err := deleteHookByPolicy(u.cfg, h, hooks.BeforeHookCreation); err != nil {
return err return err

@ -0,0 +1,109 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kubefake "helm.sh/helm/pkg/kube/fake"
"helm.sh/helm/pkg/release"
)
func upgradeAction(t *testing.T) *Upgrade {
config := actionConfigFixture(t)
upAction := NewUpgrade(config)
upAction.Namespace = "spaced"
return upAction
}
func TestUpgradeRelease_Wait(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
upAction.cfg.KubeClient = failer
upAction.Wait = true
upAction.rawValues = map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart())
req.Error(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestUpgradeRelease_Atomic(t *testing.T) {
is := assert.New(t)
req := require.New(t)
t.Run("atomic rollback succeeds", func(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
// We can't make Update error because then the rollback won't work
failer.WatchUntilReadyError = fmt.Errorf("arming key removed")
upAction.cfg.KubeClient = failer
upAction.Atomic = true
upAction.rawValues = map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart())
req.Error(err)
is.Contains(err.Error(), "arming key removed")
is.Contains(err.Error(), "atomic")
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3)
is.NoError(err)
// Should have rolled back to the previous
is.Equal(updatedRes.Info.Status, release.StatusDeployed)
})
t.Run("atomic uninstall fails", func(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "fallout"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.UpdateError = fmt.Errorf("update fail")
upAction.cfg.KubeClient = failer
upAction.Atomic = true
upAction.rawValues = map[string]interface{}{}
_, err := upAction.Run(rel.Name, buildChart())
req.Error(err)
is.Contains(err.Error(), "update fail")
is.Contains(err.Error(), "an error occurred while rolling back the release")
})
}

@ -0,0 +1,116 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package fake implements various fake KubeClients for use in testing
package fake
import (
"io"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/pkg/kube"
)
// FailingKubeClient implements KubeClient for testing purposes. It also has
// additional errors you can set to fail different functions, otherwise it
// delegates all its calls to `PrintingKubeClient`
type FailingKubeClient struct {
PrintingKubeClient
CreateError error
WaitError error
GetError error
DeleteError error
WatchUntilReadyError error
UpdateError error
BuildError error
BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error
}
// Create returns the configured error if set or prints
func (f *FailingKubeClient) Create(r io.Reader) error {
if f.CreateError != nil {
return f.CreateError
}
return f.PrintingKubeClient.Create(r)
}
// Wait returns the configured error if set or prints
func (f *FailingKubeClient) Wait(r io.Reader, d time.Duration) error {
if f.WaitError != nil {
return f.WaitError
}
return f.PrintingKubeClient.Wait(r, d)
}
// Create returns the configured error if set or prints
func (f *FailingKubeClient) Get(r io.Reader) (string, error) {
if f.GetError != nil {
return "", f.GetError
}
return f.PrintingKubeClient.Get(r)
}
// Delete returns the configured error if set or prints
func (f *FailingKubeClient) Delete(r io.Reader) error {
if f.DeleteError != nil {
return f.DeleteError
}
return f.PrintingKubeClient.Delete(r)
}
// WatchUntilReady returns the configured error if set or prints
func (f *FailingKubeClient) WatchUntilReady(r io.Reader, d time.Duration) error {
if f.WatchUntilReadyError != nil {
return f.WatchUntilReadyError
}
return f.PrintingKubeClient.WatchUntilReady(r, d)
}
// Update returns the configured error if set or prints
func (f *FailingKubeClient) Update(r, modifiedReader io.Reader, not, needed bool) error {
if f.UpdateError != nil {
return f.UpdateError
}
return f.PrintingKubeClient.Update(r, modifiedReader, not, needed)
}
// Build returns the configured error if set or prints
func (f *FailingKubeClient) Build(r io.Reader) (kube.Result, error) {
if f.BuildError != nil {
return []*resource.Info{}, f.BuildError
}
return f.PrintingKubeClient.Build(r)
}
// BuildUnstructured returns the configured error if set or prints
func (f *FailingKubeClient) BuildUnstructured(r io.Reader) (kube.Result, error) {
if f.BuildUnstructuredError != nil {
return []*resource.Info{}, f.BuildUnstructuredError
}
return f.PrintingKubeClient.Build(r)
}
// WaitAndGetCompletedPodPhase returns the configured error if set or prints
func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duration) (v1.PodPhase, error) {
if f.WaitAndGetCompletedPodPhaseError != nil {
return v1.PodSucceeded, f.WaitAndGetCompletedPodPhaseError
}
return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d)
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package kube package fake
import ( import (
"io" "io"
@ -22,6 +22,8 @@ import (
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/pkg/kube"
) )
// PrintingKubeClient implements KubeClient, but simply prints the reader to // PrintingKubeClient implements KubeClient, but simply prints the reader to
@ -68,11 +70,11 @@ func (p *PrintingKubeClient) Update(_, modifiedReader io.Reader, _, _ bool) erro
} }
// Build implements KubeClient Build. // Build implements KubeClient Build.
func (p *PrintingKubeClient) Build(_ io.Reader) (Result, error) { func (p *PrintingKubeClient) Build(_ io.Reader) (kube.Result, error) {
return []*resource.Info{}, nil return []*resource.Info{}, nil
} }
func (p *PrintingKubeClient) BuildUnstructured(_ io.Reader) (Result, error) { func (p *PrintingKubeClient) BuildUnstructured(_ io.Reader) (kube.Result, error) {
return p.Build(nil) return p.Build(nil)
} }
Loading…
Cancel
Save