mirror of https://github.com/helm/helm
- Add performOrderedInstall to deploy resources in DAG batch order - Add SplitManifestsBySubchart to partition manifests by subchart source - Add BuildInstallBatches to compute ordered deployment batches from chart metadata - Extract createAndWaitResources helper for reuse across install paths - When --wait=ordered, install hooks normally but deploy resources per subchart DAG - Falls back to standard all-at-once deploy when no sequencing metadata exists - 7 unit tests for manifest splitting and batch constructionpull/31991/head
parent
c8a23f0454
commit
1e79330b8c
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm Authors.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||||
|
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubchartManifests groups rendered manifest content by subchart name.
|
||||||
|
// The parent chart's own resources are keyed by the chart name itself.
|
||||||
|
type SubchartManifests map[string]string
|
||||||
|
|
||||||
|
// SplitManifestsBySubchart splits a rendered manifest string into per-subchart
|
||||||
|
// manifest strings based on the # Source: comments that Helm inserts during rendering.
|
||||||
|
//
|
||||||
|
// Each manifest document is attributed to a subchart by parsing the Source path:
|
||||||
|
// - "parentchart/templates/foo.yaml" → parent chart
|
||||||
|
// - "parentchart/charts/subchart/templates/bar.yaml" → "subchart"
|
||||||
|
// - "parentchart/charts/sub1/charts/sub2/templates/baz.yaml" → "sub1" (immediate child)
|
||||||
|
//
|
||||||
|
// The chartName parameter is the root chart name used to identify parent resources.
|
||||||
|
func SplitManifestsBySubchart(manifest string, chartName string) SubchartManifests {
|
||||||
|
result := make(SubchartManifests)
|
||||||
|
|
||||||
|
// Split on YAML document separator
|
||||||
|
docs := strings.Split(manifest, "---")
|
||||||
|
for _, doc := range docs {
|
||||||
|
doc = strings.TrimSpace(doc)
|
||||||
|
if doc == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
owner := chartName // default: parent chart owns this resource
|
||||||
|
// Look for # Source: comment to determine subchart
|
||||||
|
for line := range strings.SplitSeq(doc, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if sourcePath, ok := strings.CutPrefix(trimmed, "# Source: "); ok {
|
||||||
|
owner = subchartFromSourcePath(sourcePath, chartName)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing, ok := result[owner]; ok {
|
||||||
|
result[owner] = existing + "\n---\n" + doc
|
||||||
|
} else {
|
||||||
|
result[owner] = doc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// subchartFromSourcePath extracts the immediate subchart name from a Source path.
|
||||||
|
// Path format: "chartname/charts/subchartname/templates/file.yaml"
|
||||||
|
// or: "chartname/templates/file.yaml" (parent chart)
|
||||||
|
func subchartFromSourcePath(sourcePath, chartName string) string {
|
||||||
|
parts := strings.Split(sourcePath, "/")
|
||||||
|
// Find "charts" segment — the next segment is the subchart name
|
||||||
|
for i, part := range parts {
|
||||||
|
if part == "charts" && i+1 < len(parts) {
|
||||||
|
return parts[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No "charts" segment → belongs to parent chart
|
||||||
|
return chartName
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildInstallBatches constructs ordered batches of subchart names for installation.
|
||||||
|
// Returns batches where each batch can be installed in parallel, and batches
|
||||||
|
// are processed sequentially. The parent chart name is always in the last batch.
|
||||||
|
//
|
||||||
|
// If the chart has no sequencing metadata, returns nil (all resources deployed at once).
|
||||||
|
func BuildInstallBatches(chrt *chart.Chart) ([][]string, error) {
|
||||||
|
if chrt.Metadata == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any sequencing declarations exist
|
||||||
|
hasAnnotation := false
|
||||||
|
if chrt.Metadata.Annotations != nil {
|
||||||
|
_, hasAnnotation = chrt.Metadata.Annotations[chartutil.AnnotationDependsOnSubcharts]
|
||||||
|
}
|
||||||
|
|
||||||
|
hasDependsOn := false
|
||||||
|
for _, dep := range chrt.Metadata.Dependencies {
|
||||||
|
if len(dep.DependsOn) > 0 {
|
||||||
|
hasDependsOn = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasAnnotation && !hasDependsOn {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dag, err := chartutil.BuildSubchartDAG(chrt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build subchart dependency graph: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to compute installation order: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return batches, nil
|
||||||
|
}
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm Authors.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||||
|
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitManifestsBySubchart(t *testing.T) {
|
||||||
|
manifest := `---
|
||||||
|
# Source: myapp/templates/service.yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: myapp-svc
|
||||||
|
---
|
||||||
|
# Source: myapp/charts/redis/templates/deployment.yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
---
|
||||||
|
# Source: myapp/charts/redis/templates/service.yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis-svc
|
||||||
|
---
|
||||||
|
# Source: myapp/charts/nginx/templates/deployment.yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nginx`
|
||||||
|
|
||||||
|
result := SplitManifestsBySubchart(manifest, "myapp")
|
||||||
|
|
||||||
|
assert.Len(t, result, 3)
|
||||||
|
assert.Contains(t, result, "myapp")
|
||||||
|
assert.Contains(t, result, "redis")
|
||||||
|
assert.Contains(t, result, "nginx")
|
||||||
|
|
||||||
|
// Parent chart has 1 resource
|
||||||
|
assert.Contains(t, result["myapp"], "myapp-svc")
|
||||||
|
|
||||||
|
// Redis has 2 resources
|
||||||
|
assert.Contains(t, result["redis"], "redis")
|
||||||
|
assert.Contains(t, result["redis"], "redis-svc")
|
||||||
|
|
||||||
|
// Nginx has 1 resource
|
||||||
|
assert.Contains(t, result["nginx"], "nginx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitManifestsBySubchartNested(t *testing.T) {
|
||||||
|
manifest := `---
|
||||||
|
# Source: parent/charts/redis/charts/sentinel/templates/deploy.yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: sentinel`
|
||||||
|
|
||||||
|
result := SplitManifestsBySubchart(manifest, "parent")
|
||||||
|
|
||||||
|
// Nested subchart sentinel under redis → immediate child is "redis"
|
||||||
|
assert.Contains(t, result, "redis")
|
||||||
|
assert.Contains(t, result["redis"], "sentinel")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitManifestsBySubchartEmpty(t *testing.T) {
|
||||||
|
result := SplitManifestsBySubchart("", "myapp")
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitManifestsBySubchartNoSource(t *testing.T) {
|
||||||
|
manifest := `---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: orphan`
|
||||||
|
|
||||||
|
result := SplitManifestsBySubchart(manifest, "myapp")
|
||||||
|
|
||||||
|
// No Source comment → attributed to parent
|
||||||
|
assert.Contains(t, result, "myapp")
|
||||||
|
assert.Contains(t, result["myapp"], "orphan")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildInstallBatchesNoSequencing(t *testing.T) {
|
||||||
|
chrt := &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "myapp",
|
||||||
|
Dependencies: []*chart.Dependency{
|
||||||
|
{Name: "redis"},
|
||||||
|
{Name: "nginx"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
batches, err := BuildInstallBatches(chrt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, batches, "no batches when no sequencing declared")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildInstallBatchesWithDependsOn(t *testing.T) {
|
||||||
|
chrt := &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "foo",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
chartutil.AnnotationDependsOnSubcharts: `["bar", "rabbitmq"]`,
|
||||||
|
},
|
||||||
|
Dependencies: []*chart.Dependency{
|
||||||
|
{Name: "nginx"},
|
||||||
|
{Name: "rabbitmq"},
|
||||||
|
{
|
||||||
|
Name: "bar",
|
||||||
|
DependsOn: []string{"nginx", "rabbitmq"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
batches, err := BuildInstallBatches(chrt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, batches, 3)
|
||||||
|
assert.Equal(t, []string{"nginx", "rabbitmq"}, batches[0])
|
||||||
|
assert.Equal(t, []string{"bar"}, batches[1])
|
||||||
|
assert.Equal(t, []string{"foo"}, batches[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildInstallBatchesCircularDep(t *testing.T) {
|
||||||
|
chrt := &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "parent",
|
||||||
|
Dependencies: []*chart.Dependency{
|
||||||
|
{Name: "A", DependsOn: []string{"B"}},
|
||||||
|
{Name: "B", DependsOn: []string{"A"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := BuildInstallBatches(chrt)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "circular dependency")
|
||||||
|
}
|
||||||
Loading…
Reference in new issue