feat(spec): Task 6 — sequenced install action with two-level DAG ordering

Adds pkg/action/sequencing.go with GroupManifestsByDirectSubchart and
sequencedDeployment, which deploys chart resources in subchart and
resource-group topological batches. Extends action.go to expose sorted
manifests from the render pipeline via renderResourcesWithFiles. Wires
up performSequencedInstall in install.go when --wait=ordered is used.
pull/31992/head
caretak3r 8 months ago
parent 1e41788adc
commit 85488eed18

@ -220,17 +220,24 @@ func splitAndDeannotate(postrendered string) (map[string]string, error) {
//
// This code has to do with writing files to disk.
func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) {
hs, b, notes, _, err := cfg.renderResourcesWithFiles(ch, values, releaseName, outputDir, subNotes, useReleaseName, includeCrds, pr, interactWithRemote, enableDNS, hideSecret)
return hs, b, notes, err
}
// renderResourcesWithFiles is the canonical render implementation, also returning the sorted
// []Manifest slice for use by sequenced install/upgrade.
func (cfg *Configuration) renderResourcesWithFiles(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, []releaseutil.Manifest, error) {
var hs []*release.Hook
b := bytes.NewBuffer(nil)
caps, err := cfg.getCapabilities()
if err != nil {
return hs, b, "", err
return hs, b, "", nil, err
}
if ch.Metadata.KubeVersion != "" {
if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
return hs, b, "", fmt.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.Version)
return hs, b, "", nil, fmt.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.Version)
}
}
@ -243,7 +250,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
if interactWithRemote && cfg.RESTClientGetter != nil {
restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil {
return hs, b, "", err
return hs, b, "", nil, err
}
e := engine.New(restConfig)
e.EnableDNS = enableDNS
@ -259,7 +266,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
}
if err2 != nil {
return hs, b, "", err2
return hs, b, "", nil, err2
}
// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
@ -293,19 +300,19 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
// Merge files as stream of documents for sending to post renderer
merged, err := annotateAndMerge(files)
if err != nil {
return hs, b, notes, fmt.Errorf("error merging manifests: %w", err)
return hs, b, notes, nil, fmt.Errorf("error merging manifests: %w", err)
}
// Run the post renderer
postRendered, err := pr.Run(bytes.NewBufferString(merged))
if err != nil {
return hs, b, notes, fmt.Errorf("error while running post render on files: %w", err)
return hs, b, notes, nil, fmt.Errorf("error while running post render on files: %w", err)
}
// Use the file list and contents received from the post renderer
files, err = splitAndDeannotate(postRendered.String())
if err != nil {
return hs, b, notes, fmt.Errorf("error while parsing post rendered output: %w", err)
return hs, b, notes, nil, fmt.Errorf("error while parsing post rendered output: %w", err)
}
}
@ -325,7 +332,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
}
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
}
return hs, b, "", err
return hs, b, "", nil, err
}
// Aggregate all valid manifests into one big doc.
@ -338,7 +345,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
} else {
err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Filename])
if err != nil {
return hs, b, "", err
return hs, b, "", nil, err
}
fileWritten[crd.Filename] = true
}
@ -363,13 +370,13 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values,
// used by install or upgrade
err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
if err != nil {
return hs, b, "", err
return hs, b, "", nil, err
}
fileWritten[m.Name] = true
}
}
return hs, b, notes, nil
return hs, b, notes, manifests, nil
}
// RESTClientGetter gets the rest client

@ -362,8 +362,14 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
rel := i.createRelease(chrt, vals, i.Labels)
// For ordered installs, also capture the per-file manifest list for DAG grouping.
var sortedManifests []releaseutil.Manifest
var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret)
if i.WaitStrategy == kube.OrderedWaitStrategy {
rel.Hooks, manifestDoc, rel.Info.Notes, sortedManifests, err = i.cfg.renderResourcesWithFiles(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret)
} else {
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret)
}
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()
@ -457,7 +463,11 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
return rel, err
}
rel, err = i.performInstallCtx(ctx, rel, toBeAdopted, resources)
if i.WaitStrategy == kube.OrderedWaitStrategy {
rel, err = i.performSequencedInstallCtx(ctx, chrt, rel, sortedManifests)
} else {
rel, err = i.performInstallCtx(ctx, rel, toBeAdopted, resources)
}
if err != nil {
rel, err = i.failRelease(rel, err)
}
@ -486,6 +496,84 @@ func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, t
}
}
// performSequencedInstallCtx runs performSequencedInstall in a goroutine and
// respects context cancellation, mirroring the pattern in performInstallCtx.
func (i *Install) performSequencedInstallCtx(ctx context.Context, chrt *chart.Chart, rel *release.Release, manifests []releaseutil.Manifest) (*release.Release, error) {
type Msg struct {
r *release.Release
e error
}
resultChan := make(chan Msg, 1)
go func() {
i.goroutineCount.Add(1)
rel, err := i.performSequencedInstall(ctx, chrt, rel, manifests)
resultChan <- Msg{rel, err}
i.goroutineCount.Add(-1)
}()
select {
case <-ctx.Done():
return rel, ctx.Err()
case msg := <-resultChan:
return msg.r, msg.e
}
}
// performSequencedInstall deploys chart resources in DAG-ordered batches when
// --wait=ordered is active. Subcharts are deployed in dependency order, and within
// each chart, resource-groups are deployed in annotation-declared order.
func (i *Install) performSequencedInstall(ctx context.Context, chrt *chart.Chart, rel *release.Release, manifests []releaseutil.Manifest) (*release.Release, error) {
// pre-install hooks
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.WaitStrategy, i.WaitOptions, i.Timeout, i.ServerSideApply); err != nil {
return rel, fmt.Errorf("failed pre-install: %s", err)
}
}
// Determine per-batch readiness timeout (default 1 minute when unset)
readinessTimeout := i.ReadinessTimeout
if readinessTimeout <= 0 {
readinessTimeout = time.Minute
}
sd := &sequencedDeployment{
cfg: i.cfg,
disableOpenAPI: i.DisableOpenAPIValidation,
serverSideApply: i.ServerSideApply,
forceConflicts: i.ForceConflicts,
forceReplace: i.ForceReplace,
waitStrategy: i.WaitStrategy,
waitOptions: i.WaitOptions,
waitForJobs: i.WaitForJobs,
timeout: i.Timeout,
readinessTimeout: readinessTimeout,
deadline: time.Now().Add(i.Timeout),
}
if err := sd.deployChartLevel(ctx, chrt, manifests); err != nil {
return rel, err
}
// post-install hooks
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.WaitStrategy, i.WaitOptions, i.Timeout, i.ServerSideApply); err != nil {
return rel, fmt.Errorf("failed post-install: %s", err)
}
}
if len(i.Description) > 0 {
rel.SetStatus(rcommon.StatusDeployed, i.Description)
} else {
rel.SetStatus(rcommon.StatusDeployed, "Install complete")
}
if err := i.recordRelease(rel); err != nil {
i.cfg.Logger().Error("failed to record the release", slog.Any("error", err))
}
return rel, nil
}
// getGoroutineCount return the number of running routines
func (i *Install) getGoroutineCount() int32 {
return i.goroutineCount.Load()

@ -0,0 +1,269 @@
/*
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 (
"bytes"
"context"
"fmt"
"log/slog"
"strings"
"time"
chartv2 "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
)
// GroupManifestsByDirectSubchart groups manifests by the direct subchart they belong to.
// The parent chart's own manifests (templates directly under `<chartName>/templates/`) are
// returned under the empty string key "".
// Subcharts are keyed by their immediate name under `<chartName>/charts/<subchart>/`.
// Nested subcharts (e.g., `<chartName>/charts/sub/charts/nested/`) are grouped under
// the direct subchart name ("sub"), since nested sequencing is handled recursively.
func GroupManifestsByDirectSubchart(manifests []releaseutil.Manifest, chartName string) map[string][]releaseutil.Manifest {
result := make(map[string][]releaseutil.Manifest)
if chartName == "" {
// Fallback: assign everything to parent
result[""] = append(result[""], manifests...)
return result
}
chartsPrefix := chartName + "/charts/"
for _, m := range manifests {
if !strings.HasPrefix(m.Name, chartsPrefix) {
// Parent chart manifest
result[""] = append(result[""], m)
continue
}
// Extract the direct subchart name (first segment after "<chartName>/charts/")
rest := m.Name[len(chartsPrefix):]
// rest is like "subchart1/templates/deploy.yaml" or "subchart1/charts/nested/..."
idx := strings.Index(rest, "/")
if idx < 0 {
// Unlikely: a file directly under charts/ with no subdirectory
result[""] = append(result[""], m)
continue
}
subchartName := rest[:idx]
result[subchartName] = append(result[subchartName], m)
}
return result
}
// buildManifestYAML concatenates the Content fields of the given manifests into a single
// YAML stream suitable for passing to KubeClient.Build().
func buildManifestYAML(manifests []releaseutil.Manifest) string {
if len(manifests) == 0 {
return ""
}
var buf strings.Builder
for _, m := range manifests {
buf.WriteString(m.Content)
buf.WriteString("\n")
}
return buf.String()
}
// sequencedDeployment performs ordered installation of chart resources.
// It handles the two-level DAG: first subchart ordering, then resource-group
// ordering within each chart level.
type sequencedDeployment struct {
cfg *Configuration
disableOpenAPI bool
serverSideApply bool
forceConflicts bool
forceReplace bool
waitStrategy kube.WaitStrategy
waitOptions []kube.WaitOption
waitForJobs bool
timeout time.Duration
readinessTimeout time.Duration
deadline time.Time // overall operation deadline
}
// deployChartLevel deploys all resources for a single chart level in sequenced order.
// It first handles subcharts in dependency order (recursively), then deploys the
// parent chart's own resource-group batches.
func (s *sequencedDeployment) deployChartLevel(ctx context.Context, chrt *chartv2.Chart, manifests []releaseutil.Manifest) error {
// Group manifests by direct subchart
grouped := GroupManifestsByDirectSubchart(manifests, chrt.Name())
// Build subchart DAG and deploy in topological order
dag, err := chartutil.BuildSubchartDAG(chrt)
if err != nil {
return fmt.Errorf("building subchart DAG for %s: %w", chrt.Name(), err)
}
batches, err := dag.GetBatches()
if err != nil {
return fmt.Errorf("getting subchart batches for %s: %w", chrt.Name(), err)
}
// Deploy each subchart batch in order
for batchIdx, batch := range batches {
for _, subchartName := range batch {
subManifests := grouped[subchartName]
if len(subManifests) == 0 {
continue
}
// Find the subchart chart object for recursive nested sequencing
subChart := findSubchart(chrt, subchartName)
if subChart == nil {
// Subchart not found in chart object (may have been disabled or aliased differently)
// Fall back to flat resource-group deployment for these manifests
slog.Warn("subchart not found in chart dependencies; deploying without subchart sequencing",
"subchart", subchartName,
"batch", batchIdx,
)
if err := s.deployResourceGroupBatches(ctx, subManifests); err != nil {
return fmt.Errorf("deploying subchart %s resources: %w", subchartName, err)
}
continue
}
// Recursively deploy the subchart (handles its own nested subcharts and resource-groups)
if err := s.deployChartLevel(ctx, subChart, subManifests); err != nil {
return fmt.Errorf("deploying subchart %s: %w", subchartName, err)
}
}
}
// Deploy parent chart's own resources (after all subchart batches complete)
parentManifests := grouped[""]
if len(parentManifests) > 0 {
if err := s.deployResourceGroupBatches(ctx, parentManifests); err != nil {
return fmt.Errorf("deploying %s own resources: %w", chrt.Name(), err)
}
}
return nil
}
// deployResourceGroupBatches deploys manifests for a single chart level using
// resource-group annotation DAG ordering. Resources without group annotations
// (or with invalid ones) are deployed last.
func (s *sequencedDeployment) deployResourceGroupBatches(ctx context.Context, manifests []releaseutil.Manifest) error {
if len(manifests) == 0 {
return nil
}
result, warnings := releaseutil.ParseResourceGroups(manifests)
for _, w := range warnings {
slog.Warn("resource-group annotation warning", "warning", w)
}
// If there are sequenced groups, build their DAG and deploy in order
if len(result.Groups) > 0 {
dag, err := releaseutil.BuildResourceGroupDAG(result)
if err != nil {
return fmt.Errorf("building resource-group DAG: %w", err)
}
batches, err := dag.GetBatches()
if err != nil {
return fmt.Errorf("getting resource-group batches: %w", err)
}
for _, groupBatch := range batches {
var batchManifests []releaseutil.Manifest
for _, groupName := range groupBatch {
batchManifests = append(batchManifests, result.Groups[groupName]...)
}
if err := s.createAndWait(ctx, batchManifests); err != nil {
return err
}
}
}
// Deploy unsequenced resources last
if len(result.Unsequenced) > 0 {
if err := s.createAndWait(ctx, result.Unsequenced); err != nil {
return err
}
}
return nil
}
// createAndWait creates a set of manifest resources and waits for them to be ready.
// It respects both the per-batch readiness timeout and the overall operation deadline.
func (s *sequencedDeployment) createAndWait(ctx context.Context, manifests []releaseutil.Manifest) error {
if len(manifests) == 0 {
return nil
}
yaml := buildManifestYAML(manifests)
resources, err := s.cfg.KubeClient.Build(bytes.NewBufferString(yaml), !s.disableOpenAPI)
if err != nil {
return fmt.Errorf("building resource batch: %w", err)
}
if len(resources) == 0 {
return nil
}
_, err = s.cfg.KubeClient.Create(resources, kube.ClientCreateOptionServerSideApply(s.serverSideApply, false))
if err != nil {
return fmt.Errorf("creating resource batch: %w", err)
}
// Determine effective wait timeout: min(readinessTimeout, remaining time to overall deadline)
waitTimeout := s.readinessTimeout
if !s.deadline.IsZero() {
remaining := time.Until(s.deadline)
if remaining <= 0 {
return fmt.Errorf("overall timeout exceeded before waiting for resource batch")
}
if waitTimeout <= 0 || remaining < waitTimeout {
waitTimeout = remaining
}
}
if waitTimeout <= 0 {
waitTimeout = time.Minute // safe default
}
var waiter kube.Waiter
if c, ok := s.cfg.KubeClient.(kube.InterfaceWaitOptions); ok {
waiter, err = c.GetWaiterWithOptions(s.waitStrategy, s.waitOptions...)
} else {
waiter, err = s.cfg.KubeClient.GetWaiter(s.waitStrategy)
}
if err != nil {
return fmt.Errorf("getting waiter for resource batch: %w", err)
}
if s.waitForJobs {
return waiter.WaitWithJobs(resources, waitTimeout)
}
return waiter.Wait(resources, waitTimeout)
}
// findSubchart finds the subchart chart object within chrt's direct dependencies by name or alias.
func findSubchart(chrt *chartv2.Chart, nameOrAlias string) *chartv2.Chart {
for _, dep := range chrt.Dependencies() {
alias := dep.Metadata.Annotations["alias"]
if alias == "" {
alias = dep.Name()
}
if alias == nameOrAlias || dep.Name() == nameOrAlias {
return dep
}
}
return nil
}

@ -0,0 +1,192 @@
/*
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 (
"context"
"testing"
"time"
"helm.sh/helm/v4/pkg/kube"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
)
// makeTestManifest creates a minimal Manifest for testing.
func makeTestManifest(name, sourcePath string, annotations map[string]string) releaseutil.Manifest {
content := "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: " + name + "\n"
if len(annotations) > 0 {
content += " annotations:\n"
for k, v := range annotations {
content += " " + k + ": \"" + v + "\"\n"
}
}
head := &releaseutil.SimpleHead{}
head.Metadata = &struct {
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
}{
Name: name,
Annotations: annotations,
}
return releaseutil.Manifest{
Name: sourcePath,
Content: content,
Head: head,
}
}
func TestGroupManifestsByDirectSubchart(t *testing.T) {
manifests := []releaseutil.Manifest{
makeTestManifest("cm-parent", "mychart/templates/cm.yaml", nil),
makeTestManifest("deploy-db", "mychart/charts/database/templates/deploy.yaml", nil),
makeTestManifest("svc-db", "mychart/charts/database/templates/svc.yaml", nil),
makeTestManifest("cm-app", "mychart/charts/app/templates/cm.yaml", nil),
// nested subchart — belongs to "database" at parent level
makeTestManifest("cm-nested", "mychart/charts/database/charts/cache/templates/cm.yaml", nil),
}
groups := GroupManifestsByDirectSubchart(manifests, "mychart")
// Parent chart
if len(groups[""]) != 1 {
t.Errorf("expected 1 parent manifest, got %d", len(groups[""]))
}
if groups[""][0].Name != "mychart/templates/cm.yaml" {
t.Errorf("unexpected parent manifest: %s", groups[""][0].Name)
}
// database subchart (including its nested subchart at this level)
if len(groups["database"]) != 3 {
t.Errorf("expected 3 database manifests (including nested), got %d: %v",
len(groups["database"]), manifestNames(groups["database"]))
}
// app subchart
if len(groups["app"]) != 1 {
t.Errorf("expected 1 app manifest, got %d", len(groups["app"]))
}
}
func TestGroupManifestsByDirectSubchart_EmptyChartName(t *testing.T) {
// Edge case: chart name is empty — everything goes to parent
manifests := []releaseutil.Manifest{
makeTestManifest("cm", "templates/cm.yaml", nil),
}
groups := GroupManifestsByDirectSubchart(manifests, "")
if len(groups[""]) != 1 {
t.Errorf("expected 1 parent manifest for empty chart name, got %d", len(groups[""]))
}
}
func TestGroupManifestsByDirectSubchart_OnlySubcharts(t *testing.T) {
manifests := []releaseutil.Manifest{
makeTestManifest("deploy", "chart/charts/sub1/templates/deploy.yaml", nil),
makeTestManifest("svc", "chart/charts/sub2/templates/svc.yaml", nil),
}
groups := GroupManifestsByDirectSubchart(manifests, "chart")
if len(groups[""]) != 0 {
t.Errorf("expected no parent manifests, got %d", len(groups[""]))
}
if len(groups["sub1"]) != 1 {
t.Errorf("expected 1 sub1 manifest, got %d", len(groups["sub1"]))
}
if len(groups["sub2"]) != 1 {
t.Errorf("expected 1 sub2 manifest, got %d", len(groups["sub2"]))
}
}
func TestBuildManifestYAML(t *testing.T) {
manifests := []releaseutil.Manifest{
makeTestManifest("cm1", "chart/templates/cm1.yaml", nil),
makeTestManifest("cm2", "chart/templates/cm2.yaml", nil),
}
result := buildManifestYAML(manifests)
if result == "" {
t.Error("expected non-empty YAML output")
}
// Should contain content from both manifests
if !contains(result, "name: cm1") {
t.Error("expected cm1 in output")
}
if !contains(result, "name: cm2") {
t.Error("expected cm2 in output")
}
}
func TestBuildManifestYAML_Empty(t *testing.T) {
result := buildManifestYAML(nil)
if result != "" {
t.Errorf("expected empty string for nil manifests, got %q", result)
}
}
// contains checks if s contains substr.
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr))
}
func containsStr(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// TestInstallRelease_OrderedWaitStrategy verifies that --wait=ordered installs
// succeed end-to-end using the fake kube client.
func TestInstallRelease_OrderedWaitStrategy(t *testing.T) {
config := actionConfigFixture(t)
instAction := NewInstall(config)
instAction.Namespace = "spaced"
instAction.ReleaseName = "seq-test"
instAction.WaitStrategy = kube.OrderedWaitStrategy
instAction.Timeout = 5 * time.Minute
instAction.ReadinessTimeout = time.Minute
ch := buildChart(withSampleTemplates())
_, err := instAction.RunWithContext(context.Background(), ch, map[string]interface{}{})
if err != nil {
t.Fatalf("ordered install failed: %v", err)
}
}
// TestInstallRelease_OrderedWaitStrategy_NilChart ensures a nil chart doesn't panic.
func TestInstallRelease_ReadinessTimeoutValidation(t *testing.T) {
config := actionConfigFixture(t)
instAction := NewInstall(config)
instAction.Namespace = "spaced"
instAction.ReleaseName = "timeout-test"
instAction.WaitStrategy = kube.OrderedWaitStrategy
instAction.Timeout = 5
instAction.ReadinessTimeout = 10 // exceeds Timeout
ch := buildChart(withSampleTemplates())
_, err := instAction.RunWithContext(context.Background(), ch, map[string]interface{}{})
if err == nil {
t.Fatal("expected error for ReadinessTimeout > Timeout, got nil")
}
}
func manifestNames(ms []releaseutil.Manifest) []string {
names := make([]string, len(ms))
for i, m := range ms {
names[i] = m.Name
}
return names
}
Loading…
Cancel
Save