Add --hook-parallelism flag

Add --hook-parallelism flag to helm deployment hooks but not the testing hook

What this PR does / why we need it:
Adds hook parallelism with a default of 1 as a flag for applicable commands. Does not add parallelism for test hooks as that seems to be the reason why this https://github.com/helm/community/pull/165 was closed but I think there is still value having deployment hooks work in parallel.

Special notes for your reviewer:
Based off of https://github.com/helm/helm/pull/8946 by abaehremc and
https://github.com/helm/helm/pull/7792 by akhilles
pull/11803/head^2
Jeff van Dam 3 years ago
parent 5abcf74227
commit b1bade4abe

@ -157,6 +157,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install") f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install")
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.IntVar(&client.HookParallelism, "hook-parallelism", 1, "maximum number of hooks to execute in parallel")
f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production") f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet 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, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout")

@ -80,6 +80,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
f.BoolVar(&client.Force, "force", false, "force resource update through delete/recreate if needed") f.BoolVar(&client.Force, "force", false, "force resource update through delete/recreate if needed")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback")
f.IntVar(&client.HookParallelism, "hook-parallelism", 1, "maximum number of hooks to execute in parallel")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet 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, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout")

@ -70,6 +70,7 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall") f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation")
f.IntVar(&client.HookParallelism, "hook-parallelism", 1, "maximum number of hooks to execute in parallel")
f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")

@ -107,6 +107,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.Force = client.Force instClient.Force = client.Force
instClient.DryRun = client.DryRun instClient.DryRun = client.DryRun
instClient.DisableHooks = client.DisableHooks instClient.DisableHooks = client.DisableHooks
instClient.HookParallelism = client.HookParallelism
instClient.SkipCRDs = client.SkipCRDs instClient.SkipCRDs = client.SkipCRDs
instClient.Timeout = client.Timeout instClient.Timeout = client.Timeout
instClient.Wait = client.Wait instClient.Wait = client.Wait
@ -220,6 +221,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods") f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods")
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks") f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks")
f.IntVar(&client.HookParallelism, "hook-parallelism", 1, "maximum number of hooks to execute in parallel")
f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the upgrade process will not validate rendered templates against the Kubernetes OpenAPI Schema") f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the upgrade process will not validate rendered templates against the Kubernetes OpenAPI Schema")
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed when an upgrade is performed with install flag enabled. By default, CRDs are installed if not already present, when an upgrade is performed with install flag enabled") f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed when an upgrade is performed with install flag enabled. By default, CRDs are installed if not already present, when an upgrade is performed with install flag enabled")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")

@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
@ -281,3 +281,11 @@ func TestGetVersionSet(t *testing.T) {
t.Error("Non-existent version is reported found.") t.Error("Non-existent version is reported found.")
} }
} }
func withSecondHook(hookManifest string) chartOption {
return func(opts *chartOptions) {
opts.Templates = append(opts.Templates,
&chart.File{Name: "templates/hooks-test", Data: []byte(hookManifest)},
)
}
}

@ -18,6 +18,7 @@ package action
import ( import (
"bytes" "bytes"
"sort" "sort"
"sync"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -26,100 +27,139 @@ import (
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
) )
// execHook executes all of the hooks for the given hook event. // execHookEvent executes all of the hooks for the given hook event.
func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, timeout time.Duration) error { func (cfg *Configuration) execHookEvent(rl *release.Release, event release.HookEvent, timeout time.Duration, parallelism int) error {
executingHooks := []*release.Hook{} if parallelism < 1 {
parallelism = 1
}
weightedHooks := make(map[int][]*release.Hook)
for _, h := range rl.Hooks { for _, h := range rl.Hooks {
for _, e := range h.Events { for _, e := range h.Events {
if e == hook { if e == event {
executingHooks = append(executingHooks, h) // Set default delete policy to before-hook-creation
if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 {
// TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion
// resources. For all other resource types update in place if a
// resource with the same name already exists and is owned by the
// current release.
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
}
weightedHooks[h.Weight] = append(weightedHooks[h.Weight], h)
} }
} }
} }
// hooke are pre-ordered by kind, so keep order stable weights := make([]int, 0, len(weightedHooks))
sort.Stable(hookByWeight(executingHooks)) for w := range weightedHooks {
weights = append(weights, w)
for _, h := range executingHooks { // sort hooks in each weighted group by name
// Set default delete policy to before-hook-creation sort.Slice(weightedHooks[w], func(i, j int) bool {
if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 { return weightedHooks[w][i].Name < weightedHooks[w][j].Name
// TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion })
// resources. For all other resource types update in place if a }
// resource with the same name already exists and is owned by the sort.Ints(weights)
// current release.
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation} var mut sync.RWMutex
} for _, w := range weights {
sem := make(chan struct{}, parallelism)
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil { errsChan := make(chan error)
return err errs := make([]error, 0)
} for _, h := range weightedHooks[w] {
// execute hooks in parallel (with limited parallelism enforced by semaphore)
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), true) go func(h *release.Hook) {
if err != nil { sem <- struct{}{}
return errors.Wrapf(err, "unable to build kubernetes object for %s hook %s", hook, h.Path) errsChan <- cfg.execHook(rl, h, &mut, timeout)
<-sem
}(h)
} }
// collect errors
// Record the time at which the hook was applied to the cluster for range weightedHooks[w] {
h.LastRun = release.HookExecution{ if err := <-errsChan; err != nil {
StartedAt: helmtime.Now(), errs = append(errs, err)
Phase: release.HookPhaseRunning, }
} }
cfg.recordRelease(rl) if len(errs) > 0 {
return errors.Errorf("%s hook event failed with %d error(s): %s", event, len(errs), joinErrors(errs))
// As long as the implementation of WatchUntilReady does not panic, HookPhaseFailed or HookPhaseSucceeded
// should always be set by this function. If we fail to do that for any reason, then HookPhaseUnknown is
// the most appropriate value to surface.
h.LastRun.Phase = release.HookPhaseUnknown
// Create hook resources
if _, err := cfg.KubeClient.Create(resources); err != nil {
h.LastRun.CompletedAt = helmtime.Now()
h.LastRun.Phase = release.HookPhaseFailed
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
} }
}
// Watch hook resources until they have completed // If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted
err = cfg.KubeClient.WatchUntilReady(resources, timeout) // under succeeded condition. If so, then clear the corresponding resource object in each hook
// Note the time of success/failure for _, w := range weights {
h.LastRun.CompletedAt = helmtime.Now() for _, h := range weightedHooks[w] {
// Mark hook as succeeded or failed if err := cfg.deleteHookByPolicy(h, release.HookSucceeded); err != nil {
if err != nil {
h.LastRun.Phase = release.HookPhaseFailed
// If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if err := cfg.deleteHookByPolicy(h, release.HookFailed); err != nil {
return err return err
} }
return err
} }
h.LastRun.Phase = release.HookPhaseSucceeded
} }
return nil
}
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted // execHook executes a hook.
// under succeeded condition. If so, then clear the corresponding resource object in each hook func (cfg *Configuration) execHook(rl *release.Release, h *release.Hook, mut *sync.RWMutex, timeout time.Duration) (err error) {
for _, h := range executingHooks { if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded); err != nil { return err
return err }
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), true)
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for applying hook %s", h.Path)
}
// Record the time at which the hook was applied to the cluster
updateHookPhase(h, mut, release.HookPhaseRunning)
// Thread safety: exclusive lock is necessary to ensure that none of the hook structs are modified during recordRelease
mut.Lock()
cfg.recordRelease(rl)
mut.Unlock()
// As long as the implementation of WatchUntilReady does not panic, HookPhaseFailed or HookPhaseSucceeded
// should always be set by this function. If we fail to do that for any reason, then HookPhaseUnknown is
// the most appropriate value to surface.
defer func() {
if panic := recover(); panic != nil {
updateHookPhase(h, mut, release.HookPhaseUnknown)
err = errors.Errorf("panicked while executing hook %s", h.Path)
} }
}()
// Create hook resources
if _, err = cfg.KubeClient.Create(resources); err != nil {
updateHookPhase(h, mut, release.HookPhaseFailed)
return errors.Wrapf(err, "warning: hook %s failed", h.Path)
} }
// Watch hook resources until they have completed then mark hook as succeeded or failed
if err = cfg.KubeClient.WatchUntilReady(resources, timeout); err != nil {
updateHookPhase(h, mut, release.HookPhaseFailed)
// If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook.
if deleteHookErr := cfg.deleteHookByPolicy(h, release.HookFailed); deleteHookErr != nil {
return deleteHookErr
}
return err
}
updateHookPhase(h, mut, release.HookPhaseSucceeded)
return nil return nil
} }
// hookByWeight is a sorter for hooks // updateHookPhase updates the phase of a hook in a thread-safe manner.
type hookByWeight []*release.Hook func updateHookPhase(h *release.Hook, mut *sync.RWMutex, phase release.HookPhase) {
// Thread safety: shared lock is sufficient because each execHook goroutine operates on a different hook
func (x hookByWeight) Len() int { return len(x) } completedAtTime := helmtime.Now()
func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] } mut.RLock()
func (x hookByWeight) Less(i, j int) bool { startedAtTime := helmtime.Now()
if x[i].Weight == x[j].Weight { switch phase {
return x[i].Name < x[j].Name case release.HookPhaseRunning:
h.LastRun.StartedAt = startedAtTime
case release.HookPhaseSucceeded, release.HookPhaseFailed:
h.LastRun.CompletedAt = completedAtTime
} }
return x[i].Weight < x[j].Weight h.LastRun.Phase = phase
mut.RUnlock()
} }
// deleteHookByPolicy deletes a hook if the hook policy instructs it to // deleteHookByPolicy deletes a hook if the hook policy instructs it to.
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error { func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error {
// Never delete CustomResourceDefinitions; this could cause lots of // Never delete CustomResourceDefinitions; this could cause lots of
// cascading garbage collection. // cascading garbage collection.

@ -73,6 +73,7 @@ type Install struct {
CreateNamespace bool CreateNamespace bool
DryRun bool DryRun bool
DisableHooks bool DisableHooks bool
HookParallelism int
Replace bool Replace bool
Wait bool Wait bool
WaitForJobs bool WaitForJobs bool
@ -360,7 +361,7 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t
// pre-install hooks // pre-install hooks
if !i.DisableHooks { if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil { if err := i.cfg.execHookEvent(rel, release.HookPreInstall, i.Timeout, i.HookParallelism); err != nil {
i.reportToRun(c, rel, fmt.Errorf("failed pre-install: %s", err)) i.reportToRun(c, rel, fmt.Errorf("failed pre-install: %s", err))
return return
} }
@ -396,7 +397,7 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t
} }
if !i.DisableHooks { if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil { if err := i.cfg.execHookEvent(rel, release.HookPostInstall, i.Timeout, i.HookParallelism); err != nil {
i.reportToRun(c, rel, fmt.Errorf("failed post-install: %s", err)) i.reportToRun(c, rel, fmt.Errorf("failed post-install: %s", err))
return return
} }
@ -444,6 +445,7 @@ func (i *Install) failRelease(rel *release.Release, err error) (*release.Release
i.cfg.Log("Install failed and atomic is set, uninstalling release") i.cfg.Log("Install failed and atomic is set, uninstalling release")
uninstall := NewUninstall(i.cfg) uninstall := NewUninstall(i.cfg)
uninstall.DisableHooks = i.DisableHooks uninstall.DisableHooks = i.DisableHooks
uninstall.HookParallelism = i.HookParallelism
uninstall.KeepHistory = false uninstall.KeepHistory = false
uninstall.Timeout = i.Timeout uninstall.Timeout = i.Timeout
if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil { if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {

@ -717,3 +717,85 @@ func TestNameAndChartGenerateName(t *testing.T) {
}) })
} }
} }
func TestInstallRelease_HookParallelism(t *testing.T) {
is := assert.New(t)
t.Run("hook parallelism of 0 defaults to 1", func(t *testing.T) {
instAction := installAction(t)
instAction.HookParallelism = 0
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Equal(res.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Install complete")
})
t.Run("hook parallelism greater than number of hooks", func(t *testing.T) {
instAction := installAction(t)
instAction.HookParallelism = 10
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Equal(res.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Install complete")
})
t.Run("hook parallelism with multiple hooks", func(t *testing.T) {
instAction := installAction(t)
instAction.HookParallelism = 2
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSecondHook(manifestWithHook)), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Equal(res.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 2)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.Equal(rel.Hooks[1].Manifest, manifestWithHook)
is.Equal(rel.Hooks[1].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[1].Events[1], release.HookPreDelete, "Expected event 1 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Install complete")
})
}

@ -88,7 +88,7 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
rel.Hooks = executingHooks rel.Hooks = executingHooks
} }
if err := r.cfg.execHook(rel, release.HookTest, r.Timeout); err != nil { if err := r.cfg.execHookEvent(rel, release.HookTest, r.Timeout, 1); err != nil {
rel.Hooks = append(skippedHooks, rel.Hooks...) rel.Hooks = append(skippedHooks, rel.Hooks...)
r.cfg.Releases.Update(rel) r.cfg.Releases.Update(rel)
return rel, err return rel, err

@ -35,16 +35,17 @@ import (
type Rollback struct { type Rollback struct {
cfg *Configuration cfg *Configuration
Version int Version int
Timeout time.Duration Timeout time.Duration
Wait bool Wait bool
WaitForJobs bool WaitForJobs bool
DisableHooks bool DisableHooks bool
DryRun bool HookParallelism int
Recreate bool // will (if true) recreate pods after a rollback. DryRun bool
Force bool // will (if true) force resource upgrade through uninstall/recreate if needed Recreate bool // will (if true) recreate pods after a rollback.
CleanupOnFail bool Force bool // will (if true) force resource upgrade through uninstall/recreate if needed
MaxHistory int // MaxHistory limits the maximum number of revisions saved per release CleanupOnFail bool
MaxHistory int // MaxHistory limits the maximum number of revisions saved per release
} }
// NewRollback creates a new Rollback object with the given configuration. // NewRollback creates a new Rollback object with the given configuration.
@ -157,7 +158,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// pre-rollback hooks // pre-rollback hooks
if !r.DisableHooks { if !r.DisableHooks {
if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.Timeout); err != nil { if err := r.cfg.execHookEvent(targetRelease, release.HookPreRollback, r.Timeout, r.HookParallelism); err != nil {
return targetRelease, err return targetRelease, err
} }
} else { } else {
@ -224,7 +225,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// post-rollback hooks // post-rollback hooks
if !r.DisableHooks { if !r.DisableHooks {
if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.Timeout); err != nil { if err := r.cfg.execHookEvent(targetRelease, release.HookPostRollback, r.Timeout, r.HookParallelism); err != nil {
return targetRelease, err return targetRelease, err
} }
} }

@ -0,0 +1,274 @@
/*
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 (
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
func rollbackAction(t *testing.T) *Rollback {
config := actionConfigFixture(t)
rollAction := NewRollback(config)
return rollAction
}
func TestRollbackRelease_HookParallelism(t *testing.T) {
is := assert.New(t)
t.Run("hook parallelism of 0 defaults to 1", func(t *testing.T) {
rollAction := rollbackAction(t)
rollAction.HookParallelism = 0
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := helmtime.Now()
rel1 := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Release 1",
},
Chart: sampleChart,
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPreRollback,
release.HookPostRollback,
},
},
},
}
rel2 := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Release 2",
},
Chart: sampleChart,
Version: 2,
}
err := rollAction.cfg.Releases.Create(rel1)
is.NoError(err)
err = rollAction.cfg.Releases.Create(rel2)
is.NoError(err)
err = rollAction.Run(rel2.Name)
if err != nil {
t.Fatalf("Failed rollback: %s", err)
}
rel, err := rollAction.cfg.Releases.Get(rel1.Name, 3)
is.NoError(err)
is.Equal("nuketown", rel.Name, "Expected release name.")
is.Len(rel.Hooks, 1)
is.Equal(manifestWithHook, rel.Hooks[0].Manifest)
is.Equal(release.HookPreRollback, rel.Hooks[0].Events[0])
is.Equal(release.HookPostRollback, rel.Hooks[0].Events[1])
is.Equal(0, len(rel.Manifest))
is.Equal("Rollback to 1", rel.Info.Description)
})
t.Run("hook parallelism greater than number of hooks", func(t *testing.T) {
rollAction := rollbackAction(t)
rollAction.HookParallelism = 10
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := helmtime.Now()
rel1 := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Release 1",
},
Chart: sampleChart,
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPreRollback,
release.HookPostRollback,
},
},
},
}
rel2 := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Release 2",
},
Chart: sampleChart,
Version: 2,
}
err := rollAction.cfg.Releases.Create(rel1)
is.NoError(err)
err = rollAction.cfg.Releases.Create(rel2)
is.NoError(err)
err = rollAction.Run(rel2.Name)
if err != nil {
t.Fatalf("Failed rollback: %s", err)
}
rel, err := rollAction.cfg.Releases.Get(rel1.Name, 3)
is.NoError(err)
is.Equal("nuketown", rel.Name, "Expected release name.")
is.Len(rel.Hooks, 1)
is.Equal(manifestWithHook, rel.Hooks[0].Manifest)
is.Equal(release.HookPreRollback, rel.Hooks[0].Events[0])
is.Equal(release.HookPostRollback, rel.Hooks[0].Events[1])
is.Equal(0, len(rel.Manifest))
is.Equal("Rollback to 1", rel.Info.Description)
})
t.Run("hook parallelism with multiple hooks", func(t *testing.T) {
rollAction := rollbackAction(t)
rollAction.HookParallelism = 10
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
withSecondHook(manifestWithHook),
)
now := helmtime.Now()
rel1 := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Release 1",
},
Chart: sampleChart,
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPreRollback,
release.HookPostRollback,
},
},
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPreRollback,
release.HookPostRollback,
},
},
},
}
rel2 := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Release 2",
},
Chart: sampleChart,
Version: 2,
}
err := rollAction.cfg.Releases.Create(rel1)
is.NoError(err)
err = rollAction.cfg.Releases.Create(rel2)
is.NoError(err)
err = rollAction.Run(rel2.Name)
if err != nil {
t.Fatalf("Failed rollback: %s", err)
}
rel, err := rollAction.cfg.Releases.Get(rel1.Name, 3)
is.NoError(err)
is.Equal("nuketown", rel.Name, "Expected release name.")
is.Len(rel.Hooks, 2)
is.Equal(manifestWithHook, rel.Hooks[0].Manifest)
is.Equal(release.HookPreRollback, rel.Hooks[0].Events[0])
is.Equal(release.HookPostRollback, rel.Hooks[0].Events[1])
is.Equal(manifestWithHook, rel.Hooks[1].Manifest)
is.Equal(release.HookPreRollback, rel.Hooks[1].Events[0])
is.Equal(release.HookPostRollback, rel.Hooks[1].Events[1])
is.Equal(0, len(rel.Manifest))
is.Equal("Rollback to 1", rel.Info.Description)
})
}

@ -35,12 +35,13 @@ import (
type Uninstall struct { type Uninstall struct {
cfg *Configuration cfg *Configuration
DisableHooks bool DisableHooks bool
DryRun bool HookParallelism int
KeepHistory bool DryRun bool
Wait bool KeepHistory bool
Timeout time.Duration Wait bool
Description string Timeout time.Duration
Description string
} }
// NewUninstall creates a new Uninstall object with the given configuration. // NewUninstall creates a new Uninstall object with the given configuration.
@ -99,7 +100,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res := &release.UninstallReleaseResponse{Release: rel} res := &release.UninstallReleaseResponse{Release: rel}
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPreDelete, u.Timeout); err != nil { if err := u.cfg.execHookEvent(rel, release.HookPreDelete, u.Timeout, u.HookParallelism); err != nil {
return res, err return res, err
} }
} else { } else {
@ -132,7 +133,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
} }
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil { if err := u.cfg.execHookEvent(rel, release.HookPostDelete, u.Timeout, u.HookParallelism); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
} }

@ -22,8 +22,10 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"helm.sh/helm/v3/pkg/chart"
kubefake "helm.sh/helm/v3/pkg/kube/fake" kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
) )
func uninstallAction(t *testing.T) *Uninstall { func uninstallAction(t *testing.T) *Uninstall {
@ -95,3 +97,197 @@ func TestUninstallRelease_Wait(t *testing.T) {
is.Contains(err.Error(), "U timed out") is.Contains(err.Error(), "U timed out")
is.Equal(res.Release.Info.Status, release.StatusUninstalled) is.Equal(res.Release.Info.Status, release.StatusUninstalled)
} }
func TestUninstallRelease_HookParallelism(t *testing.T) {
is := assert.New(t)
t.Run("hook parallelism of 0 defaults to 1", func(t *testing.T) {
unAction := uninstallAction(t)
unAction.HookParallelism = 0
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := time.Now()
rel := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Named Release Stub",
},
Chart: sampleChart,
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPreDelete,
release.HookPostDelete,
},
},
},
}
err := unAction.cfg.Releases.Create(rel)
is.NoError(err)
res, err := unAction.Run(rel.Name)
if err != nil {
t.Fatalf("Failed uninstall: %s", err)
}
is.Equal("nuketown", res.Release.Name, "Expected release name.")
is.Len(res.Release.Hooks, 1)
is.Equal(manifestWithHook, res.Release.Hooks[0].Manifest)
is.Equal(release.HookPreDelete, res.Release.Hooks[0].Events[0])
is.Equal(release.HookPostDelete, res.Release.Hooks[0].Events[1])
is.Equal(0, len(res.Release.Manifest))
is.Equal("Uninstallation complete", res.Release.Info.Description)
})
t.Run("hook parallelism greater than number of hooks", func(t *testing.T) {
unAction := uninstallAction(t)
unAction.HookParallelism = 10
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := time.Now()
rel := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Named Release Stub",
},
Chart: sampleChart,
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPreDelete,
release.HookPostDelete,
},
},
},
}
err := unAction.cfg.Releases.Create(rel)
is.NoError(err)
res, err := unAction.Run(rel.Name)
if err != nil {
t.Fatalf("Failed uninstall: %s", err)
}
is.Equal("nuketown", res.Release.Name, "Expected release name.")
is.Len(res.Release.Hooks, 1)
is.Equal(manifestWithHook, res.Release.Hooks[0].Manifest)
is.Equal(release.HookPreDelete, res.Release.Hooks[0].Events[0])
is.Equal(release.HookPostDelete, res.Release.Hooks[0].Events[1])
is.Equal(0, len(res.Release.Manifest))
is.Equal("Uninstallation complete", res.Release.Info.Description)
})
t.Run("hook parallelism with multiple hooks", func(t *testing.T) {
unAction := uninstallAction(t)
unAction.HookParallelism = 2
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
withSecondHook(manifestWithHook),
)
now := time.Now()
rel := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Named Release Stub",
},
Chart: sampleChart,
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPreDelete,
release.HookPostDelete,
},
},
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPreDelete,
release.HookPostDelete,
},
},
},
}
err := unAction.cfg.Releases.Create(rel)
is.NoError(err)
res, err := unAction.Run(rel.Name)
if err != nil {
t.Fatalf("Failed uninstall: %s", err)
}
is.Equal("nuketown", res.Release.Name, "Expected release name.")
is.Len(rel.Hooks, 2)
is.Equal(manifestWithHook, res.Release.Hooks[0].Manifest)
is.Equal(release.HookPreDelete, res.Release.Hooks[0].Events[0])
is.Equal(release.HookPostDelete, res.Release.Hooks[0].Events[1])
is.Equal(manifestWithHook, res.Release.Hooks[1].Manifest)
is.Equal(release.HookPreDelete, res.Release.Hooks[1].Events[0])
is.Equal(release.HookPostDelete, res.Release.Hooks[1].Events[1])
is.Equal(0, len(res.Release.Manifest))
is.Equal("Uninstallation complete", res.Release.Info.Description)
})
}

@ -61,24 +61,25 @@ type Upgrade struct {
Namespace string Namespace string
// SkipCRDs skips installing CRDs when install flag is enabled during upgrade // SkipCRDs skips installing CRDs when install flag is enabled during upgrade
SkipCRDs bool SkipCRDs bool
// Timeout is the timeout for this operation // Timeout is the timeout for this operation
Timeout time.Duration Timeout time.Duration
// Wait determines whether the wait operation should be performed after the upgrade is requested. // Wait determines whether the wait operation should be performed after the upgrade is requested.
Wait bool Wait bool
// WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested. // WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested.
WaitForJobs bool WaitForJobs bool
// DisableHooks disables hook processing if set to true. // DisableHooks disables hook processing if set to true.
DisableHooks bool DisableHooks bool
// DryRun controls whether the operation is prepared, but not executed. HookParallelism int
// DryRun controls whether the operation is prepared, but not executed.
// If `true`, the upgrade is prepared but not performed. // If `true`, the upgrade is prepared but not performed.
DryRun bool DryRun bool
// Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway.
// //
// This should be used with caution. // This should be used with caution.
Force bool Force bool
// ResetValues will reset the values to the chart's built-ins rather than merging with existing. // ResetValues will reset the values to the chart's built-ins rather than merging with existing.
ResetValues bool ResetValues bool
// ReuseValues will re-use the user's last supplied values. // ReuseValues will re-use the user's last supplied values.
ReuseValues bool ReuseValues bool
// Recreate will (if true) recreate pods after a rollback. // Recreate will (if true) recreate pods after a rollback.
Recreate bool Recreate bool
@ -367,7 +368,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
// pre-upgrade hooks // pre-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil { if err := u.cfg.execHookEvent(upgradedRelease, release.HookPreUpgrade, u.Timeout, u.HookParallelism); err != nil {
u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
return return
} }
@ -413,7 +414,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
// post-upgrade hooks // post-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil { if err := u.cfg.execHookEvent(upgradedRelease, release.HookPostUpgrade, u.Timeout, u.HookParallelism); err != nil {
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
return return
} }
@ -478,6 +479,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
rollin.Wait = true rollin.Wait = true
rollin.WaitForJobs = u.WaitForJobs rollin.WaitForJobs = u.WaitForJobs
rollin.DisableHooks = u.DisableHooks rollin.DisableHooks = u.DisableHooks
rollin.HookParallelism = u.HookParallelism
rollin.Recreate = u.Recreate rollin.Recreate = u.Recreate
rollin.Force = u.Force rollin.Force = u.Force
rollin.Timeout = u.Timeout rollin.Timeout = u.Timeout

@ -388,3 +388,169 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
is.Equal(updatedRes.Info.Status, release.StatusDeployed) is.Equal(updatedRes.Info.Status, release.StatusDeployed)
} }
func TestUpgradeRelease_HookParallelism(t *testing.T) {
is := assert.New(t)
t.Run("hook parallelism of 0 defaults to 1", func(t *testing.T) {
upAction := upgradeAction(t)
upAction.HookParallelism = 0
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := helmtime.Now()
rel := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Named Release Stub",
},
Chart: sampleChart,
Version: 1,
}
err := upAction.cfg.Releases.Create(rel)
is.NoError(err)
res, err := upAction.Run(rel.Name, sampleChart, map[string]interface{}{})
if err != nil {
t.Fatalf("Failed upgrade: %s", err)
}
is.Equal(res.Name, "nuketown", "Expected release name.")
rel, err = upAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: sample/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Upgrade complete")
})
t.Run("hook parallelism greater than number of hooks", func(t *testing.T) {
upAction := upgradeAction(t)
upAction.HookParallelism = 10
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := helmtime.Now()
rel := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Named Release Stub",
},
Chart: sampleChart,
Version: 1,
}
err := upAction.cfg.Releases.Create(rel)
is.NoError(err)
res, err := upAction.Run(rel.Name, sampleChart, map[string]interface{}{})
if err != nil {
t.Fatalf("Failed upgrade: %s", err)
}
is.Equal(res.Name, "nuketown", "Expected release name.")
rel, err = upAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: sample/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Upgrade complete")
})
t.Run("hook parallelism with multiple hooks", func(t *testing.T) {
upAction := upgradeAction(t)
upAction.HookParallelism = 2
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
withSecondHook(manifestWithHook),
)
now := helmtime.Now()
rel := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Named Release Stub",
},
Chart: sampleChart,
Version: 1,
}
err := upAction.cfg.Releases.Create(rel)
is.NoError(err)
res, err := upAction.Run(rel.Name, sampleChart, map[string]interface{}{})
if err != nil {
t.Fatalf("Failed upgrade: %s", err)
}
is.Equal(res.Name, "nuketown", "Expected release name.")
rel, err = upAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 2)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.Equal(rel.Hooks[1].Manifest, manifestWithHook)
is.Equal(rel.Hooks[1].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[1].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: sample/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Upgrade complete")
})
}

Loading…
Cancel
Save