mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1045 lines
35 KiB
1045 lines
35 KiB
/*
|
|
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 sequence
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
chart "helm.sh/helm/v4/pkg/chart/v2"
|
|
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
|
|
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
|
|
)
|
|
|
|
func TestBuild_NilChart_FlatPlan(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("db", "parent/templates/db.yaml", groupAnnotations("db")),
|
|
makeManifest("app", "parent/templates/app.yaml", groupAnnotations("app", "db")),
|
|
makeManifest("plain", "parent/templates/plain.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(nil, manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []ChartLevel{{Path: "", Depth: 0}}, plan.Levels)
|
|
assert.Equal(t, []batchSummary{
|
|
{ChartPath: "", Depth: 0, Kind: BatchKindGroups, Groups: []string{"db"}},
|
|
{ChartPath: "", Depth: 0, Kind: BatchKindGroups, Groups: []string{"app"}},
|
|
{ChartPath: "", Depth: 0, Kind: BatchKindUnsequenced, Groups: []string{""}},
|
|
}, batchSummaries(plan))
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_EmptyChart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
plan, err := Build(newChart("parent"), nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []ChartLevel{{Path: "parent", Depth: 0}}, plan.Levels)
|
|
assert.Empty(t, plan.Batches)
|
|
assert.Empty(t, plan.Warnings)
|
|
}
|
|
|
|
func TestGroupManifestsByDirectSubchart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("parent", "parent/templates/one.yaml", nil),
|
|
makeManifest("db", "parent/charts/database/templates/one.yaml", nil),
|
|
makeManifest("cache", "parent/charts/database/charts/cache/templates/one.yaml", nil),
|
|
}
|
|
|
|
grouped := GroupManifestsByDirectSubchart(manifests, "parent")
|
|
|
|
require.Len(t, grouped[""], 1)
|
|
require.Len(t, grouped["database"], 2)
|
|
}
|
|
|
|
// TestGroupManifestsByDirectSubchart_Nested verifies that when called
|
|
// with a deeper chartPath (i.e., during recursion into a subchart), nested
|
|
// grandchildren are routed to the correct subchart key instead of being merged
|
|
// into the parent batch.
|
|
func TestGroupManifestsByDirectSubchart_Nested(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("db-own", "parent/charts/database/templates/one.yaml", nil),
|
|
makeManifest("cache", "parent/charts/database/charts/cache/templates/one.yaml", nil),
|
|
}
|
|
|
|
grouped := GroupManifestsByDirectSubchart(manifests, "parent/charts/database")
|
|
|
|
require.Len(t, grouped[""], 1, "database's own resources should be under the empty key")
|
|
require.Len(t, grouped["cache"], 1, "nested cache subchart should be routed under its own key")
|
|
require.Equal(t, "parent/charts/database/templates/one.yaml", grouped[""][0].Name)
|
|
require.Equal(t, "parent/charts/database/charts/cache/templates/one.yaml", grouped["cache"][0].Name)
|
|
}
|
|
|
|
func TestFindSubchart(t *testing.T) {
|
|
// makeSubchart constructs a chart with the given chart-name, its own
|
|
// Metadata.Name, which BuildSubchartDAG and the legacy lookup resolve
|
|
// against.
|
|
makeSubchart := func(chartName string) *chart.Chart {
|
|
return &chart.Chart{
|
|
Metadata: &chart.Metadata{
|
|
APIVersion: "v1",
|
|
Name: chartName,
|
|
Version: "0.1.0",
|
|
},
|
|
}
|
|
}
|
|
|
|
// makeParent attaches subcharts as dependencies and declares the
|
|
// parent's Metadata.Dependencies, which carry the Alias field.
|
|
makeParent := func(deps []*chart.Chart, metaDeps []*chart.Dependency) *chart.Chart {
|
|
parent := &chart.Chart{
|
|
Metadata: &chart.Metadata{
|
|
APIVersion: "v1",
|
|
Name: "parent",
|
|
Version: "0.1.0",
|
|
Dependencies: metaDeps,
|
|
},
|
|
}
|
|
for _, d := range deps {
|
|
parent.AddDependency(d)
|
|
}
|
|
return parent
|
|
}
|
|
|
|
t.Run("resolves by chart name when no alias is declared", func(t *testing.T) {
|
|
db := makeSubchart("database")
|
|
parent := makeParent(
|
|
[]*chart.Chart{db},
|
|
[]*chart.Dependency{{Name: "database"}},
|
|
)
|
|
|
|
got := FindSubchart(parent, "database")
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "database", got.Name())
|
|
})
|
|
|
|
t.Run("resolves by alias when alias is declared", func(t *testing.T) {
|
|
postgres := makeSubchart("postgres")
|
|
parent := makeParent(
|
|
[]*chart.Chart{postgres},
|
|
[]*chart.Dependency{{Name: "postgres", Alias: "db"}},
|
|
)
|
|
|
|
got := FindSubchart(parent, "db")
|
|
require.NotNil(t, got, "alias lookup should resolve to the underlying chart")
|
|
assert.Equal(t, "postgres", got.Name())
|
|
})
|
|
|
|
t.Run("resolves by underlying chart name even when an alias is declared", func(t *testing.T) {
|
|
// An alias does not hide the chart's real name. Manifests rendered
|
|
// under the chart's actual chart-name path should still resolve.
|
|
postgres := makeSubchart("postgres")
|
|
parent := makeParent(
|
|
[]*chart.Chart{postgres},
|
|
[]*chart.Dependency{{Name: "postgres", Alias: "db"}},
|
|
)
|
|
|
|
got := FindSubchart(parent, "postgres")
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "postgres", got.Name())
|
|
})
|
|
|
|
t.Run("alias collides with another chart's real name, first match wins", func(t *testing.T) {
|
|
// dep1: chart "foo" aliased as "bar".
|
|
// dep2: chart "bar" with no alias.
|
|
// Query "bar" must resolve deterministically. Current contract:
|
|
// iteration order over Dependencies() is preserved, so the first
|
|
// dep whose effective name (alias or real) matches the query wins.
|
|
foo := makeSubchart("foo")
|
|
bar := makeSubchart("bar")
|
|
parent := makeParent(
|
|
[]*chart.Chart{foo, bar},
|
|
[]*chart.Dependency{
|
|
{Name: "foo", Alias: "bar"},
|
|
{Name: "bar"},
|
|
},
|
|
)
|
|
|
|
got := FindSubchart(parent, "bar")
|
|
require.NotNil(t, got, "collision must resolve, not return nil")
|
|
assert.Equal(t, "foo", got.Name(),
|
|
"first matching dependency wins; aliased 'foo' is declared before raw 'bar'")
|
|
|
|
// And the raw name "foo" must still resolve to chart "foo" even
|
|
// though its effective name has been shifted by the alias.
|
|
gotFoo := FindSubchart(parent, "foo")
|
|
require.NotNil(t, gotFoo)
|
|
assert.Equal(t, "foo", gotFoo.Name())
|
|
})
|
|
|
|
t.Run("returns nil when not found", func(t *testing.T) {
|
|
db := makeSubchart("database")
|
|
parent := makeParent(
|
|
[]*chart.Chart{db},
|
|
[]*chart.Dependency{{Name: "database"}},
|
|
)
|
|
|
|
assert.Nil(t, FindSubchart(parent, "nonexistent"))
|
|
})
|
|
|
|
t.Run("returns nil when parent has no dependencies", func(t *testing.T) {
|
|
parent := &chart.Chart{
|
|
Metadata: &chart.Metadata{APIVersion: "v1", Name: "parent", Version: "0.1.0"},
|
|
}
|
|
assert.Nil(t, FindSubchart(parent, "anything"))
|
|
})
|
|
}
|
|
|
|
func TestBuild_NoAnnotations_SingleFlatBatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("a", "parent/templates/a.yaml", nil),
|
|
makeManifest("b", "parent/templates/b.yaml", nil),
|
|
makeManifest("c", "parent/templates/c.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
require.Len(t, plan.Batches, 1)
|
|
assert.Equal(t, BatchKindUnsequenced, plan.Batches[0].Kind)
|
|
assert.Equal(t, manifestNames(manifests), manifestNames(plan.Batches[0].Manifests()))
|
|
assert.True(t, plan.Batches[0].Wait)
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_ResourceGroupOrdering(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("db", "parent/templates/db.yaml", groupAnnotations("db")),
|
|
makeManifest("app", "parent/templates/app.yaml", groupAnnotations("app", "db")),
|
|
makeManifest("plain", "parent/templates/plain.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []batchSummary{
|
|
{ChartPath: "parent", Depth: 0, Kind: BatchKindGroups, Groups: []string{"db"}},
|
|
{ChartPath: "parent", Depth: 0, Kind: BatchKindGroups, Groups: []string{"app"}},
|
|
{ChartPath: "parent", Depth: 0, Kind: BatchKindUnsequenced, Groups: []string{""}},
|
|
}, batchSummaries(plan))
|
|
for _, batch := range plan.Batches {
|
|
assert.True(t, batch.Wait)
|
|
}
|
|
assert.Empty(t, plan.Batches[0].LeafGroups)
|
|
assert.Equal(t, []string{"app"}, plan.Batches[1].LeafGroups)
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_LeafGroups_Diamond(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("base", "parent/templates/base.yaml", groupAnnotations("base")),
|
|
makeManifest("left", "parent/templates/left.yaml", groupAnnotations("left", "base")),
|
|
makeManifest("right", "parent/templates/right.yaml", groupAnnotations("right", "base")),
|
|
makeManifest("top", "parent/templates/top.yaml", groupAnnotations("top", "left", "right")),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, [][]string{{"base"}, {"left", "right"}, {"top"}}, batchGroupNames(plan))
|
|
assert.Empty(t, plan.Batches[0].LeafGroups)
|
|
assert.Empty(t, plan.Batches[1].LeafGroups)
|
|
assert.Equal(t, []string{"top"}, plan.Batches[2].LeafGroups)
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_NestedSubcharts_ThreeLevels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
grand := newChart("grand")
|
|
child := newChart("child", enabledDependency("grand"))
|
|
child.SetDependencies(grand)
|
|
parent := newChart("parent", enabledDependency("child"))
|
|
parent.SetDependencies(child)
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("parent", "parent/templates/parent.yaml", nil),
|
|
makeManifest("child", "parent/charts/child/templates/child.yaml", nil),
|
|
makeManifest("grand", "parent/charts/child/charts/grand/templates/grand.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(parent, manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{
|
|
"parent/charts/child/charts/grand",
|
|
"parent/charts/child",
|
|
"parent",
|
|
}, batchChartPaths(plan))
|
|
assert.Equal(t, []int{2, 1, 0}, batchDepths(plan))
|
|
assert.Equal(t, []ChartLevel{
|
|
{Path: "parent", Depth: 0, SubchartBatches: [][]string{{"child"}}},
|
|
{Path: "parent/charts/child", Depth: 1, SubchartBatches: [][]string{{"grand"}}},
|
|
{Path: "parent/charts/child/charts/grand", Depth: 2},
|
|
}, plan.Levels)
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_SubchartDependencyOrder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parent := newChart(
|
|
"parent",
|
|
enabledDependency("postgres"),
|
|
enabledDependency("rabbitmq", "postgres"),
|
|
enabledDependency("app", "rabbitmq"),
|
|
)
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("app", "parent/charts/app/templates/app.yaml", nil),
|
|
makeManifest("rabbitmq", "parent/charts/rabbitmq/templates/rabbitmq.yaml", nil),
|
|
makeManifest("postgres", "parent/charts/postgres/templates/postgres.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(parent, manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"parent/charts/postgres", "parent/charts/rabbitmq", "parent/charts/app"}, batchChartPaths(plan))
|
|
require.Len(t, plan.Levels, 4)
|
|
assert.Equal(t, [][]string{{"postgres"}, {"rabbitmq"}, {"app"}}, plan.Levels[0].SubchartBatches)
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_Aliases_RealPipeline(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parent := pipelineChart(
|
|
pipelineDependency("postgres", "primary-db"),
|
|
pipelineDependency("app", "", "postgres"),
|
|
)
|
|
require.NoError(t, chartutil.ProcessDependencies(parent, map[string]any{}))
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("app", "parent/charts/app/templates/app.yaml", nil),
|
|
makeManifest("primary", "parent/charts/primary-db/templates/primary.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(parent, manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"parent/charts/primary-db", "parent/charts/app"}, batchChartPaths(plan))
|
|
require.NotEmpty(t, plan.Levels)
|
|
assert.Equal(t, [][]string{{"primary-db"}, {"app"}}, plan.Levels[0].SubchartBatches)
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_SubchartCycle_Fatal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parent := newChart("parent", enabledDependency("a", "b"), enabledDependency("b", "a"))
|
|
plan, err := Build(parent, []releaseutil.Manifest{makeManifest("a", "parent/charts/a/templates/a.yaml", nil)})
|
|
require.Error(t, err)
|
|
assert.Nil(t, plan)
|
|
assert.Contains(t, err.Error(), "cycle")
|
|
}
|
|
|
|
func TestBuild_UnknownDependsOnRef_Fatal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parent := newChart("parent", enabledDependency("app", "missing"))
|
|
plan, err := Build(parent, nil)
|
|
require.Error(t, err)
|
|
assert.Nil(t, plan)
|
|
assert.Contains(t, err.Error(), `depends-on unknown or disabled subchart "missing"`)
|
|
}
|
|
|
|
func TestBuild_ResourceGroupCycle_Fatal_NestedLevel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parent := newChart("parent", enabledDependency("child"))
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("a", "parent/charts/child/templates/a.yaml", groupAnnotations("a", "b")),
|
|
makeManifest("b", "parent/charts/child/templates/b.yaml", groupAnnotations("b", "a")),
|
|
}
|
|
|
|
plan, err := Build(parent, manifests)
|
|
require.Error(t, err)
|
|
assert.Nil(t, plan)
|
|
assert.Contains(t, err.Error(), "cycle")
|
|
assert.Contains(t, err.Error(), "parent/charts/child")
|
|
}
|
|
|
|
func TestBuild_MultiGroupAssignment_Fatal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeTypedManifest("same", "parent/templates/one.yaml", "v1", "ConfigMap", "default", groupAnnotations("one")),
|
|
makeTypedManifest("same", "parent/templates/two.yaml", "v1", "ConfigMap", "default", groupAnnotations("two")),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.Error(t, err)
|
|
assert.Nil(t, plan)
|
|
assert.Contains(t, err.Error(), "assigned to multiple resource groups")
|
|
}
|
|
|
|
func TestBuild_IsolatedGroupDemoted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("db", "parent/templates/db.yaml", groupAnnotations("db")),
|
|
makeManifest("app", "parent/templates/app.yaml", groupAnnotations("app", "db")),
|
|
makeManifest("plain", "parent/templates/plain.yaml", nil),
|
|
makeManifest("metrics", "parent/templates/metrics.yaml", groupAnnotations("metrics")),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, [][]string{{"db"}, {"app"}, {""}}, batchGroupNames(plan))
|
|
assert.NotContains(t, slices.Concat(batchGroupNames(plan)[:2]...), "metrics")
|
|
require.Len(t, plan.Batches, 3)
|
|
assert.Equal(t, BatchKindUnsequenced, plan.Batches[2].Kind)
|
|
assert.ElementsMatch(t, []string{"parent/templates/plain.yaml", "parent/templates/metrics.yaml"}, manifestNames(plan.Batches[2].Manifests()))
|
|
assertWarningContains(t, plan, "parent", "isolated", "metrics")
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_TwoGroupChain_NotDemoted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("db", "parent/templates/db.yaml", groupAnnotations("db")),
|
|
makeManifest("app", "parent/templates/app.yaml", groupAnnotations("app", "db")),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, [][]string{{"db"}, {"app"}}, batchGroupNames(plan))
|
|
assertNoWarningContains(t, plan, "isolated")
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_SingleGroup_NotDemoted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("solo", "parent/templates/solo.yaml", groupAnnotations("solo")),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, [][]string{{"solo"}}, batchGroupNames(plan))
|
|
assert.Equal(t, BatchKindGroups, plan.Batches[0].Kind)
|
|
assertNoWarningContains(t, plan, "isolated")
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_AllGroupsIsolated_AllDemoted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("beta", "parent/templates/beta.yaml", groupAnnotations("beta")),
|
|
makeManifest("alpha", "parent/templates/alpha.yaml", groupAnnotations("alpha")),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
require.Len(t, plan.Batches, 1)
|
|
assert.Equal(t, BatchKindUnsequenced, plan.Batches[0].Kind)
|
|
assert.Equal(t, []string{"parent/templates/alpha.yaml", "parent/templates/beta.yaml"}, manifestNames(plan.Batches[0].Manifests()))
|
|
require.Len(t, plan.Warnings, 2)
|
|
assert.Contains(t, plan.Warnings[0].Message, "alpha")
|
|
assert.Contains(t, plan.Warnings[1].Message, "beta")
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_MissingGroupDep_TransitiveDemotion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("app", "parent/templates/app.yaml", groupAnnotations("app", "nope")),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
require.Len(t, plan.Batches, 1)
|
|
assert.Equal(t, BatchKindUnsequenced, plan.Batches[0].Kind)
|
|
assert.Equal(t, []string{"parent/templates/app.yaml"}, manifestNames(plan.Batches[0].Manifests()))
|
|
assertWarningContains(t, plan, "parent", "depends-on non-existent group")
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_InvalidDependsOnJSON_Demoted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
annotations := groupAnnotations("app")
|
|
annotations[releaseutil.AnnotationDependsOnResourceGroups] = "not-json"
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("app", "parent/templates/app.yaml", annotations),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
require.Len(t, plan.Batches, 1)
|
|
assert.Equal(t, BatchKindUnsequenced, plan.Batches[0].Kind)
|
|
assert.Equal(t, []string{"parent/templates/app.yaml"}, manifestNames(plan.Batches[0].Manifests()))
|
|
assertWarningContains(t, plan, "parent", "invalid JSON")
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_UndeclaredSubchartIncluded(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parent := newChart("parent", enabledDependency("declared"))
|
|
parent.AddDependency(newChart("vendored"))
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("declared", "parent/charts/declared/templates/declared.yaml", nil),
|
|
makeManifest("vendored", "parent/charts/vendored/templates/vendored.yaml", nil),
|
|
makeManifest("parent", "parent/templates/parent.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(parent, manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"parent/charts/declared", "parent/charts/vendored", "parent"}, batchChartPaths(plan))
|
|
require.NotEmpty(t, plan.Levels)
|
|
assert.Equal(t, []string{"vendored"}, plan.Levels[0].Undeclared)
|
|
assertWarningContains(t, plan, "parent", "not declared")
|
|
assertPlanComplete(t, plan, manifests)
|
|
|
|
reversedPaths := batchChartPaths(plan.Reverse())
|
|
assert.Contains(t, reversedPaths, "parent/charts/vendored")
|
|
}
|
|
|
|
func TestBuild_UnresolvableSubchart_StructuralFallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("db", "parent/charts/ghost/templates/db.yaml", groupAnnotations("db")),
|
|
makeManifest("app", "parent/charts/ghost/templates/app.yaml", groupAnnotations("app", "db")),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
// ghost is undeclared in parent's metadata (warned as such, unchanged),
|
|
// but a single unresolved subchart is fully recovered structurally, so
|
|
// there is no sibling-order warning.
|
|
assertWarningContains(t, plan, "parent", "not declared")
|
|
assertNoWarningContains(t, plan, "name order")
|
|
require.Len(t, plan.Levels, 2)
|
|
assert.Equal(t, []string{"ghost"}, plan.Levels[0].Unresolved)
|
|
assert.Equal(t, ChartLevel{Path: "parent/charts/ghost", Depth: 1}, plan.Levels[1])
|
|
assert.Equal(t, []string{"parent/charts/ghost", "parent/charts/ghost"}, batchChartPaths(plan))
|
|
assert.Equal(t, []int{1, 1}, batchDepths(plan))
|
|
assert.Equal(t, [][]string{{"db"}, {"app"}}, batchGroupNames(plan))
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
// storageRoundTrip encodes and decodes a chart the way the release storage
|
|
// codec does (json.Marshal in pkg/storage/driver): the unexported loaded
|
|
// dependency tree is dropped, only exported fields survive.
|
|
func storageRoundTrip(t *testing.T, c *chart.Chart) *chart.Chart {
|
|
t.Helper()
|
|
|
|
encoded, err := json.Marshal(c)
|
|
require.NoError(t, err)
|
|
decoded := &chart.Chart{}
|
|
require.NoError(t, json.Unmarshal(encoded, decoded))
|
|
require.Empty(t, decoded.Dependencies(), "release codec is expected to drop the loaded dependency tree")
|
|
return decoded
|
|
}
|
|
|
|
// TestBuild_StorageDecodedChart_ThreeLevels reproduces bead xmn: a 3-level
|
|
// chart decoded from release storage (as uninstall and rollback receive it)
|
|
// must yield the same batch order the freshly loaded chart produced at
|
|
// install time, not fail building the subchart DAG.
|
|
func TestBuild_StorageDecodedChart_ThreeLevels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
grand := newChart("grand")
|
|
child := newChart("child", enabledDependency("grand"))
|
|
child.SetDependencies(grand)
|
|
child.Metadata.Annotations = map[string]string{chartutil.AnnotationDependsOnSubcharts: `["grand"]`}
|
|
parent := newChart("parent", enabledDependency("child"))
|
|
parent.SetDependencies(child)
|
|
parent.Metadata.Annotations = map[string]string{chartutil.AnnotationDependsOnSubcharts: `["child"]`}
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("parent", "parent/templates/parent.yaml", nil),
|
|
makeManifest("child", "parent/charts/child/templates/child.yaml", nil),
|
|
makeManifest("grand", "parent/charts/child/charts/grand/templates/grand.yaml", nil),
|
|
}
|
|
|
|
fresh, err := Build(parent, manifests)
|
|
require.NoError(t, err)
|
|
|
|
plan, err := Build(storageRoundTrip(t, parent), manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, batchChartPaths(fresh), batchChartPaths(plan))
|
|
assert.Equal(t, []string{
|
|
"parent/charts/child/charts/grand",
|
|
"parent/charts/child",
|
|
"parent",
|
|
}, batchChartPaths(plan))
|
|
assert.Equal(t, []int{2, 1, 0}, batchDepths(plan))
|
|
assert.Equal(t, []ChartLevel{
|
|
{Path: "parent", Depth: 0, SubchartBatches: [][]string{{"child"}}, Unresolved: []string{"child"}},
|
|
{Path: "parent/charts/child", Depth: 1, SubchartBatches: [][]string{{"grand"}}},
|
|
{Path: "parent/charts/child/charts/grand", Depth: 2},
|
|
}, plan.Levels)
|
|
assert.Empty(t, plan.Warnings)
|
|
assertPlanComplete(t, plan, manifests)
|
|
|
|
assert.Equal(t, []string{
|
|
"parent",
|
|
"parent/charts/child",
|
|
"parent/charts/child/charts/grand",
|
|
}, batchChartPaths(plan.Reverse()))
|
|
}
|
|
|
|
// TestBuild_StorageDecodedChart_AliasedSiblingOrder runs the real
|
|
// ProcessDependencies pipeline (alias rename + depends-on resolution), then
|
|
// storage-decodes the chart: root-level sibling order and alias resolution
|
|
// must survive via the stored metadata.
|
|
func TestBuild_StorageDecodedChart_AliasedSiblingOrder(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parent := pipelineChart(
|
|
pipelineDependency("postgres", "primary-db"),
|
|
pipelineDependency("app", "", "postgres"),
|
|
)
|
|
require.NoError(t, chartutil.ProcessDependencies(parent, map[string]any{}))
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("app", "parent/charts/app/templates/app.yaml", nil),
|
|
makeManifest("primary", "parent/charts/primary-db/templates/primary.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(storageRoundTrip(t, parent), manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"parent/charts/primary-db", "parent/charts/app"}, batchChartPaths(plan))
|
|
require.NotEmpty(t, plan.Levels)
|
|
assert.Equal(t, [][]string{{"primary-db"}, {"app"}}, plan.Levels[0].SubchartBatches)
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
// TestBuild_StorageDecodedChart_DisabledDepNotResurrected: a dependency
|
|
// disabled by condition at install time is pruned from Metadata.Dependencies
|
|
// by ProcessDependencies BEFORE the release is stored, so trusting the stored
|
|
// metadata cannot resurrect it at uninstall.
|
|
func TestBuild_StorageDecodedChart_DisabledDepNotResurrected(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
parent := pipelineChart(
|
|
&chart.Dependency{Name: "cache", Version: "0.1.0", Condition: "cache.enabled"},
|
|
&chart.Dependency{Name: "db", Version: "0.1.0"},
|
|
&chart.Dependency{Name: "app", Version: "0.1.0", DependsOn: []string{"db"}},
|
|
)
|
|
require.NoError(t, chartutil.ProcessDependencies(parent, map[string]any{
|
|
"cache": map[string]any{"enabled": false},
|
|
}))
|
|
|
|
decoded := storageRoundTrip(t, parent)
|
|
for _, dep := range decoded.Metadata.Dependencies {
|
|
require.NotEqual(t, "cache", dep.Name, "disabled dependency must be pruned from stored metadata")
|
|
}
|
|
|
|
// The disabled subchart was never rendered, so no manifests exist for it.
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("app", "parent/charts/app/templates/app.yaml", nil),
|
|
makeManifest("db", "parent/charts/db/templates/db.yaml", nil),
|
|
makeManifest("parent", "parent/templates/parent.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(decoded, manifests)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"parent/charts/db", "parent/charts/app", "parent"}, batchChartPaths(plan))
|
|
for _, level := range plan.Levels {
|
|
assert.NotContains(t, level.Path, "cache")
|
|
}
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
// TestBuild_StorageDecodedChart_NestedSiblingOrderWarning: depends-on edges
|
|
// between SIBLING subcharts of a NESTED level live in that level's Chart.yaml,
|
|
// which storage does not preserve. The structural walk orders such siblings
|
|
// by name and says so.
|
|
func TestBuild_StorageDecodedChart_NestedSiblingOrderWarning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
grandA := newChart("g-a")
|
|
grandB := newChart("g-b")
|
|
child := newChart("child", enabledDependency("g-a"), enabledDependency("g-b", "g-a"))
|
|
child.SetDependencies(grandA, grandB)
|
|
parent := newChart("parent", enabledDependency("child"))
|
|
parent.SetDependencies(child)
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("parent", "parent/templates/parent.yaml", nil),
|
|
makeManifest("child", "parent/charts/child/templates/child.yaml", nil),
|
|
makeManifest("g-a", "parent/charts/child/charts/g-a/templates/a.yaml", nil),
|
|
makeManifest("g-b", "parent/charts/child/charts/g-b/templates/b.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(storageRoundTrip(t, parent), manifests)
|
|
require.NoError(t, err)
|
|
assertWarningContains(t, plan, "parent/charts/child", "name order")
|
|
assert.Equal(t, []string{
|
|
"parent/charts/child/charts/g-a",
|
|
"parent/charts/child/charts/g-b",
|
|
"parent/charts/child",
|
|
"parent",
|
|
}, batchChartPaths(plan))
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_HookManifestNotFiltered(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("hook", "parent/templates/hook.yaml", map[string]string{"helm.sh/hook": "pre-install"}),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
require.Len(t, plan.Batches, 1)
|
|
assert.Equal(t, []string{"parent/templates/hook.yaml"}, manifestNames(plan.Batches[0].Manifests()))
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_HasCustomReadiness(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
groupA := groupAnnotations("a")
|
|
groupA[releaseutil.AnnotationReadinessSuccess] = `{.status.ready} == true`
|
|
groupA[releaseutil.AnnotationReadinessFailure] = `{.status.failed} == true`
|
|
groupB := groupAnnotations("b", "a")
|
|
groupB[releaseutil.AnnotationReadinessSuccess] = `{.status.ready} == true`
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("a", "parent/templates/a.yaml", groupA),
|
|
makeManifest("b", "parent/templates/b.yaml", groupB),
|
|
makeManifest("plain", "parent/templates/plain.yaml", nil),
|
|
}
|
|
|
|
plan, err := Build(newChart("parent"), manifests)
|
|
require.NoError(t, err)
|
|
require.Len(t, plan.Batches, 3)
|
|
assert.True(t, plan.Batches[0].HasCustomReadiness)
|
|
assert.False(t, plan.Batches[1].HasCustomReadiness)
|
|
assert.False(t, plan.Batches[2].HasCustomReadiness)
|
|
assertWarningContains(t, plan, "parent", "only one of")
|
|
assertPlanComplete(t, plan, manifests)
|
|
}
|
|
|
|
func TestBuild_Deterministic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
chartOne, manifestsOne := kitchenSinkFixture()
|
|
chartTwo, manifestsTwo := kitchenSinkFixture()
|
|
|
|
planOne, err := Build(chartOne, manifestsOne)
|
|
require.NoError(t, err)
|
|
planTwo, err := Build(chartTwo, manifestsTwo)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, planOne, planTwo)
|
|
}
|
|
|
|
func TestBuild_Completeness_KitchenSink(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
chrt, manifests := kitchenSinkFixture()
|
|
plan, err := Build(chrt, manifests)
|
|
require.NoError(t, err)
|
|
assertPlanComplete(t, plan, manifests)
|
|
for _, batch := range plan.Batches {
|
|
assert.NotEmpty(t, batch.Manifests())
|
|
if batch.Kind == BatchKindUnsequenced {
|
|
require.Len(t, batch.Groups, 1)
|
|
assert.Empty(t, batch.Groups[0].Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
type batchSummary struct {
|
|
ChartPath string
|
|
Depth int
|
|
Kind BatchKind
|
|
Groups []string
|
|
}
|
|
|
|
func summarizeBatch(batch Batch) batchSummary {
|
|
return batchSummary{
|
|
ChartPath: batch.ChartPath,
|
|
Depth: batch.Depth,
|
|
Kind: batch.Kind,
|
|
Groups: groupNames(batch),
|
|
}
|
|
}
|
|
|
|
func batchSummaries(plan *Plan) []batchSummary {
|
|
summaries := make([]batchSummary, 0, len(plan.Batches))
|
|
for _, batch := range plan.Batches {
|
|
summaries = append(summaries, summarizeBatch(batch))
|
|
}
|
|
return summaries
|
|
}
|
|
|
|
func batchGroupNames(plan *Plan) [][]string {
|
|
names := make([][]string, 0, len(plan.Batches))
|
|
for _, batch := range plan.Batches {
|
|
names = append(names, groupNames(batch))
|
|
}
|
|
return names
|
|
}
|
|
|
|
func batchChartPaths(plan *Plan) []string {
|
|
paths := make([]string, 0, len(plan.Batches))
|
|
for _, batch := range plan.Batches {
|
|
paths = append(paths, batch.ChartPath)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func batchDepths(plan *Plan) []int {
|
|
depths := make([]int, 0, len(plan.Batches))
|
|
for _, batch := range plan.Batches {
|
|
depths = append(depths, batch.Depth)
|
|
}
|
|
return depths
|
|
}
|
|
|
|
func groupNames(batch Batch) []string {
|
|
names := make([]string, 0, len(batch.Groups))
|
|
for _, group := range batch.Groups {
|
|
names = append(names, group.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
func manifestNames(manifests []releaseutil.Manifest) []string {
|
|
names := make([]string, 0, len(manifests))
|
|
for _, manifest := range manifests {
|
|
names = append(names, manifest.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
func assertPlanComplete(t *testing.T, plan *Plan, manifests []releaseutil.Manifest) {
|
|
t.Helper()
|
|
|
|
require.NotNil(t, plan)
|
|
expected := make(map[string]int, len(manifests))
|
|
for _, manifest := range manifests {
|
|
expected[manifest.Name]++
|
|
}
|
|
|
|
actual := make(map[string]int, len(manifests))
|
|
var total int
|
|
for _, batch := range plan.Batches {
|
|
for _, manifest := range batch.Manifests() {
|
|
actual[manifest.Name]++
|
|
total++
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, len(manifests), total)
|
|
assert.Equal(t, expected, actual)
|
|
}
|
|
|
|
func assertWarningContains(t *testing.T, plan *Plan, chartPath string, parts ...string) {
|
|
t.Helper()
|
|
|
|
for _, warning := range plan.Warnings {
|
|
if warning.ChartPath != chartPath {
|
|
continue
|
|
}
|
|
matches := true
|
|
for _, part := range parts {
|
|
if !strings.Contains(warning.Message, part) {
|
|
matches = false
|
|
break
|
|
}
|
|
}
|
|
if matches {
|
|
return
|
|
}
|
|
}
|
|
assert.Failf(t, "missing warning", "chartPath=%q parts=%v warnings=%v", chartPath, parts, plan.Warnings)
|
|
}
|
|
|
|
func assertNoWarningContains(t *testing.T, plan *Plan, part string) {
|
|
t.Helper()
|
|
|
|
for _, warning := range plan.Warnings {
|
|
assert.NotContains(t, warning.Message, part)
|
|
}
|
|
}
|
|
|
|
func makeManifest(name, sourcePath string, annotations map[string]string) releaseutil.Manifest {
|
|
return makeTypedManifest(name, sourcePath, "v1", "ConfigMap", "", annotations)
|
|
}
|
|
|
|
func makeTypedManifest(name, sourcePath, version, kind, namespace string, annotations map[string]string) releaseutil.Manifest {
|
|
annotations = maps.Clone(annotations)
|
|
var content strings.Builder
|
|
fmt.Fprintf(&content, "apiVersion: %s\nkind: %s\nmetadata:\n name: %s\n", version, kind, name)
|
|
if namespace != "" {
|
|
fmt.Fprintf(&content, " namespace: %s\n", namespace)
|
|
}
|
|
if len(annotations) > 0 {
|
|
content.WriteString(" annotations:\n")
|
|
for _, key := range slices.Sorted(maps.Keys(annotations)) {
|
|
fmt.Fprintf(&content, " %s: %q\n", key, annotations[key])
|
|
}
|
|
}
|
|
|
|
head := &releaseutil.SimpleHead{
|
|
Version: version,
|
|
Kind: kind,
|
|
Metadata: &struct {
|
|
Name string `json:"name"`
|
|
Namespace string `json:"namespace,omitempty"`
|
|
Annotations map[string]string `json:"annotations"`
|
|
}{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
Annotations: annotations,
|
|
},
|
|
}
|
|
|
|
return releaseutil.Manifest{
|
|
Name: sourcePath,
|
|
Content: content.String(),
|
|
Head: head,
|
|
}
|
|
}
|
|
|
|
func groupAnnotations(group string, deps ...string) map[string]string {
|
|
annotations := map[string]string{
|
|
releaseutil.AnnotationResourceGroup: group,
|
|
}
|
|
if len(deps) > 0 {
|
|
encoded, err := json.Marshal(deps)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
annotations[releaseutil.AnnotationDependsOnResourceGroups] = string(encoded)
|
|
}
|
|
return annotations
|
|
}
|
|
|
|
func newChart(name string, deps ...*chart.Dependency) *chart.Chart {
|
|
c := &chart.Chart{
|
|
Metadata: &chart.Metadata{
|
|
Name: name,
|
|
Version: "0.1.0",
|
|
APIVersion: chart.APIVersionV2,
|
|
Dependencies: deps,
|
|
},
|
|
}
|
|
for _, dep := range deps {
|
|
if dep == nil || !dep.Enabled {
|
|
continue
|
|
}
|
|
subName := dep.Alias
|
|
if subName == "" {
|
|
subName = dep.Name
|
|
}
|
|
c.AddDependency(newChart(subName))
|
|
}
|
|
return c
|
|
}
|
|
|
|
func enabledDependency(name string, dependsOn ...string) *chart.Dependency {
|
|
return &chart.Dependency{
|
|
Name: name,
|
|
Version: "0.1.0",
|
|
Enabled: true,
|
|
DependsOn: dependsOn,
|
|
}
|
|
}
|
|
|
|
func aliasedDependency(name, alias string, dependsOn ...string) *chart.Dependency {
|
|
return &chart.Dependency{
|
|
Name: name,
|
|
Version: "0.1.0",
|
|
Alias: alias,
|
|
Enabled: true,
|
|
DependsOn: dependsOn,
|
|
}
|
|
}
|
|
|
|
func pipelineChart(deps ...*chart.Dependency) *chart.Chart {
|
|
c := &chart.Chart{
|
|
Metadata: &chart.Metadata{
|
|
Name: "parent",
|
|
Version: "0.1.0",
|
|
APIVersion: chart.APIVersionV2,
|
|
Dependencies: deps,
|
|
},
|
|
}
|
|
added := make(map[string]bool)
|
|
for _, dep := range deps {
|
|
if added[dep.Name] {
|
|
continue
|
|
}
|
|
added[dep.Name] = true
|
|
c.AddDependency(&chart.Chart{
|
|
Metadata: &chart.Metadata{
|
|
Name: dep.Name,
|
|
Version: "0.1.0",
|
|
APIVersion: chart.APIVersionV2,
|
|
},
|
|
})
|
|
}
|
|
return c
|
|
}
|
|
|
|
func pipelineDependency(name, alias string, dependsOn ...string) *chart.Dependency {
|
|
return &chart.Dependency{
|
|
Name: name,
|
|
Version: "0.1.0",
|
|
Alias: alias,
|
|
DependsOn: dependsOn,
|
|
}
|
|
}
|
|
|
|
func kitchenSinkFixture() (*chart.Chart, []releaseutil.Manifest) {
|
|
primary := newChart("primary-db")
|
|
worker := newChart("worker")
|
|
app := newChart("app", enabledDependency("worker"))
|
|
app.SetDependencies(worker)
|
|
vendored := newChart("vendored")
|
|
|
|
parent := newChart(
|
|
"parent",
|
|
aliasedDependency("postgres", "primary-db"),
|
|
enabledDependency("app", "primary-db"),
|
|
)
|
|
parent.SetDependencies(primary, app, vendored)
|
|
|
|
manifests := []releaseutil.Manifest{
|
|
makeManifest("primary", "parent/charts/primary-db/templates/primary.yaml", nil),
|
|
makeManifest("worker", "parent/charts/app/charts/worker/templates/worker.yaml", nil),
|
|
makeManifest("app-sub", "parent/charts/app/templates/app.yaml", groupAnnotations("app-sub")),
|
|
makeManifest("vendored", "parent/charts/vendored/templates/vendored.yaml", nil),
|
|
makeManifest("db", "parent/templates/db.yaml", groupAnnotations("db")),
|
|
makeManifest("app", "parent/templates/app.yaml", groupAnnotations("app", "db")),
|
|
makeManifest("metrics", "parent/templates/metrics.yaml", groupAnnotations("metrics")),
|
|
makeManifest("plain", "parent/templates/plain.yaml", nil),
|
|
}
|
|
return parent, manifests
|
|
}
|