pull/8946/merge
abaehreMC 5 years ago committed by GitHub
commit 5ebed7ae13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -137,6 +137,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present") f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install") f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install")
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")

@ -79,6 +79,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
f := cmd.Flags() f := cmd.Flags()
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.IntVar(&client.HookParallelism, "hook-parallelism", 1, "maximum number of hooks to execute in parallel")
f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)") f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)")
return cmd return cmd

@ -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.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails")

@ -73,6 +73,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.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.StringVar(&client.Description, "description", "", "add a custom description") f.StringVar(&client.Description, "description", "", "add a custom description")

@ -100,6 +100,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.ChartPathOptions = client.ChartPathOptions instClient.ChartPathOptions = client.ChartPathOptions
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
@ -173,6 +174,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)")

@ -1168,6 +1168,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
helm.sh/helm v1.2.1 h1:Jrn7kKQqQ/hnFWZEX+9pMFvYqFexkzrBnGqYBmIph7c=
helm.sh/helm v2.16.12+incompatible h1:nQfifk10KcpAGD1RJaNZVW/fWiqluV0JMuuDwdba4rw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -262,6 +262,14 @@ func withKube(version string) chartOption {
} }
} }
func withSecondHook(hookManifest string) chartOption {
return func(opts *chartOptions) {
opts.Templates = append(opts.Templates,
&chart.File{Name: "templates/hooks-test", Data: []byte(hookManifest)},
)
}
}
// releaseStub creates a release stub, complete with the chartStub as its chart. // releaseStub creates a release stub, complete with the chartStub as its chart.
func releaseStub() *release.Release { func releaseStub() *release.Release {
return namedReleaseStub("angry-panda", release.StatusDeployed) return namedReleaseStub("angry-panda", release.StatusDeployed)

@ -18,6 +18,7 @@ package action
import ( import (
"bytes" "bytes"
"sort" "sort"
"sync"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -26,22 +27,16 @@ 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)
}
}
}
// hooke are pre-ordered by kind, so keep order stable
sort.Stable(hookByWeight(executingHooks))
for _, h := range executingHooks {
// Set default delete policy to before-hook-creation // Set default delete policy to before-hook-creation
if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 { if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 {
// TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion // TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion
@ -50,76 +45,145 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// current release. // current release.
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation} h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
} }
weightedHooks[h.Weight] = append(weightedHooks[h.Weight], h)
}
}
}
weights := make([]int, 0, len(weightedHooks))
for w := range weightedHooks {
weights = append(weights, w)
// sort hooks in each weighted group by name
sort.Slice(weightedHooks[w], func(i, j int) bool {
return weightedHooks[w][i].Name < weightedHooks[w][j].Name
})
}
sort.Ints(weights)
var mut sync.RWMutex
for _, w := range weights {
sem := make(chan struct{}, parallelism)
errsChan := make(chan error)
errs := make([]error, 0)
for _, h := range weightedHooks[w] {
// execute hooks in parallel (with limited parallelism enforced by semaphore)
go func(h *release.Hook) {
sem <- struct{}{}
errsChan <- cfg.execHook(rl, h, &mut, timeout)
<-sem
}(h)
}
// collect errors
for range weightedHooks[w] {
if err := <-errsChan; err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Errorf("%s hook event failed with %d error(s): %s", event, len(errs), joinErrors(errs))
}
}
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, w := range weights {
for _, h := range weightedHooks[w] {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded); err != nil {
return err
}
}
}
return nil
}
// // hooke are pre-ordered by kind, so keep order stable
// sort.Stable(hookByWeight(executingHooks))
// for _, h := range executingHooks {
// // 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}
// }
// if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil {
// 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 %s hook %s", hook, h.Path)
// }
// execHook executes a hook.
func (cfg *Configuration) execHook(rl *release.Release, h *release.Hook, mut *sync.RWMutex, timeout time.Duration) (err error) {
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil { if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil {
return err return err
} }
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), true) resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), true)
if err != nil { if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for %s hook %s", hook, h.Path) 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 // Record the time at which the hook was applied to the cluster
h.LastRun = release.HookExecution{ updateHookPhase(h, mut, release.HookPhaseRunning)
StartedAt: helmtime.Now(), // Thread safety: exclusive lock is necessary to ensure that none of the hook structs are modified during recordRelease
Phase: release.HookPhaseRunning, mut.Lock()
}
cfg.recordRelease(rl) cfg.recordRelease(rl)
mut.Unlock()
// As long as the implementation of WatchUntilReady does not panic, HookPhaseFailed or HookPhaseSucceeded // 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 // 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. // the most appropriate value to surface.
h.LastRun.Phase = release.HookPhaseUnknown 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 // Create hook resources
if _, err := cfg.KubeClient.Create(resources); err != nil { if _, err = cfg.KubeClient.Create(resources); err != nil {
h.LastRun.CompletedAt = helmtime.Now() updateHookPhase(h, mut, release.HookPhaseFailed)
h.LastRun.Phase = release.HookPhaseFailed return errors.Wrapf(err, "warning: hook %s failed", h.Path)
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
} }
// Watch hook resources until they have completed // Watch hook resources until they have completed then mark hook as succeeded or failed
err = cfg.KubeClient.WatchUntilReady(resources, timeout) if err = cfg.KubeClient.WatchUntilReady(resources, timeout); err != nil {
// Note the time of success/failure updateHookPhase(h, mut, release.HookPhaseFailed)
h.LastRun.CompletedAt = helmtime.Now()
// Mark hook as succeeded or failed
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 // 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 // under failed condition. If so, then clear the corresponding resource object in the hook.
if err := cfg.deleteHookByPolicy(h, release.HookFailed); err != nil { if deleteHookErr := cfg.deleteHookByPolicy(h, release.HookFailed); deleteHookErr != nil {
return err return deleteHookErr
} }
return err return err
} }
h.LastRun.Phase = release.HookPhaseSucceeded updateHookPhase(h, mut, release.HookPhaseSucceeded)
}
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, h := range executingHooks {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded); err != nil {
return err
}
}
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
return x[i].Weight < x[j].Weight case release.HookPhaseSucceeded, release.HookPhaseFailed:
h.LastRun.CompletedAt = completedAtTime
}
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.

@ -75,6 +75,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
Devel bool Devel bool
@ -326,7 +327,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// 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 {
return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err)) return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
} }
} }
@ -352,7 +353,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
} }
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 {
return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err)) return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
} }
} }
@ -383,6 +384,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 {

@ -403,6 +403,88 @@ func TestInstallRelease_Atomic(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")
})
}
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

@ -35,6 +35,7 @@ import (
type ReleaseTesting struct { type ReleaseTesting struct {
cfg *Configuration cfg *Configuration
Timeout time.Duration Timeout time.Duration
HookParallelism int
// Used for fetching logs from test pods // Used for fetching logs from test pods
Namespace string Namespace string
} }
@ -62,7 +63,7 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
return rel, err return rel, err
} }
if err := r.cfg.execHook(rel, release.HookTest, r.Timeout); err != nil { if err := r.cfg.execHookEvent(rel, release.HookTest, r.Timeout, r.HookParallelism); err != nil {
r.cfg.Releases.Update(rel) r.cfg.Releases.Update(rel)
return rel, err return rel, err
} }

@ -39,6 +39,7 @@ type Rollback struct {
Timeout time.Duration Timeout time.Duration
Wait bool Wait bool
DisableHooks bool DisableHooks bool
HookParallelism int
DryRun bool DryRun bool
Recreate bool // will (if true) recreate pods after a rollback. Recreate bool // will (if true) recreate pods after a rollback.
Force bool // will (if true) force resource upgrade through uninstall/recreate if needed Force bool // will (if true) force resource upgrade through uninstall/recreate if needed
@ -156,7 +157,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 {
@ -209,7 +210,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
} }
} }

@ -35,6 +35,7 @@ type Uninstall struct {
cfg *Configuration cfg *Configuration
DisableHooks bool DisableHooks bool
HookParallelism int
DryRun bool DryRun bool
KeepHistory bool KeepHistory bool
Timeout time.Duration Timeout time.Duration
@ -97,7 +98,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 {
@ -114,7 +115,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res.Info = kept res.Info = kept
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)
} }
} }

@ -66,6 +66,8 @@ type Upgrade struct {
Wait bool Wait bool
// DisableHooks disables hook processing if set to true. // DisableHooks disables hook processing if set to true.
DisableHooks bool DisableHooks bool
// HookParallelism controls the maximum number of hooks to run in parallel
HookParallelism int
// DryRun controls whether the operation is prepared, but not executed. // 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
@ -305,7 +307,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// 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 {
return u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) return u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
} }
} else { } else {
@ -337,7 +339,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// 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 {
return u.failRelease(upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) return u.failRelease(upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
} }
} }
@ -401,6 +403,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
rollin.Version = filteredHistory[0].Version rollin.Version = filteredHistory[0].Version
rollin.Wait = true rollin.Wait = true
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

@ -273,3 +273,169 @@ func TestUpgradeRelease_Pending(t *testing.T) {
_, err := upAction.Run(rel.Name, buildChart(), vals) _, err := upAction.Run(rel.Name, buildChart(), vals)
req.Contains(err.Error(), "progress", err) req.Contains(err.Error(), "progress", err)
} }
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 := 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,
}
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 := 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,
}
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 := 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,
}
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