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