mirror of https://github.com/helm/helm
Introduces the foundation for HIP-0025 resource creation sequencing: - Add DependsOn []string field to v2 and v3 Dependency structs - Implement DAG with topological sort, cycle detection, and batch layering - Add BuildSubchartDAG to construct dependency graphs from Chart.yaml - Parse helm.sh/depends-on/subcharts annotation from chart metadata - 23 unit tests covering linear, diamond, parallel, orphan, and cycle casespull/31991/head
parent
2daac031c6
commit
01fdc96bf5
@ -0,0 +1,84 @@
|
|||||||
|
# PLAN.md — worktree/1: HIP-0025 Foundation (Chart Metadata + DAG)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
HIP-0025 introduces native resource and subchart sequencing to Helm v4.
|
||||||
|
This worktree implements the **foundation layer**: chart metadata extensions
|
||||||
|
and DAG construction needed before action/CLI integration.
|
||||||
|
|
||||||
|
**Spec source:** `hip-0025/hip-0025.md`, `hip-0025/subchart-sequencing.md`
|
||||||
|
|
||||||
|
## Scope (this branch)
|
||||||
|
|
||||||
|
### 1. Chart Metadata Extension — `DependsOn` field
|
||||||
|
- Add `DependsOn []string` to `Dependency` struct in both v2 and v3
|
||||||
|
- Supports `depends-on` YAML key in Chart.yaml dependencies list
|
||||||
|
- Add validation: each DependsOn entry must reference a known dependency name/alias
|
||||||
|
- Sanitize DependsOn strings in `Validate()`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `pkg/chart/v2/dependency.go` — add field + validation
|
||||||
|
- `internal/chart/v3/dependency.go` — add field + validation
|
||||||
|
|
||||||
|
### 2. Annotation Parsing — `helm.sh/depends-on/subcharts`
|
||||||
|
- Parse `helm.sh/depends-on/subcharts` from `Metadata.Annotations`
|
||||||
|
- Already available: `Annotations map[string]string` exists on both v2/v3 Metadata
|
||||||
|
- Utility function to extract and parse the annotation value (JSON array of strings)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- New: `pkg/chart/v2/util/sequencing.go` — annotation constants, parse helpers
|
||||||
|
- New: `internal/chart/v3/util/sequencing.go` — v3 variant
|
||||||
|
|
||||||
|
### 3. DAG Construction & Topological Sort
|
||||||
|
- Build subchart dependency graph from:
|
||||||
|
- `Dependency.DependsOn` field entries
|
||||||
|
- `Metadata.Annotations["helm.sh/depends-on/subcharts"]` entries
|
||||||
|
- Topological sort → produce ordered batches (layers)
|
||||||
|
- Circular dependency detection with clear error messages
|
||||||
|
- Orphaned subchart handling (no deps → deployed with parent in final batch)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- New: `pkg/chart/v2/util/dag.go` — DAG struct, AddNode, AddEdge, TopologicalSort, DetectCycles
|
||||||
|
- New: `internal/chart/v3/util/dag.go` — v3 variant (or shared via common)
|
||||||
|
|
||||||
|
### 4. Integration with ProcessDependencies
|
||||||
|
- After existing enable/disable logic, build subchart DAG
|
||||||
|
- Validate DAG (no cycles, all DependsOn refs resolve)
|
||||||
|
- Attach DAG to chart processing result for downstream use
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `pkg/chart/v2/util/dependencies.go` — add DAG building after ProcessDependencies
|
||||||
|
- `internal/chart/v3/util/dependencies.go` — same
|
||||||
|
|
||||||
|
### 5. Tests
|
||||||
|
- Unit tests for DependsOn parsing and validation
|
||||||
|
- Unit tests for DAG construction (linear, diamond, parallel, orphan patterns)
|
||||||
|
- Unit tests for circular dependency detection
|
||||||
|
- Unit tests for annotation parsing
|
||||||
|
- Integration test with chart fixtures
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `pkg/chart/v2/util/dag_test.go`
|
||||||
|
- `pkg/chart/v2/util/sequencing_test.go`
|
||||||
|
- `pkg/chart/v2/dependency_test.go` (extend existing)
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-unit # Full unit test suite
|
||||||
|
go test ./pkg/chart/v2/... # Chart v2 tests
|
||||||
|
go test ./pkg/chart/v2/util/... # DAG + sequencing tests
|
||||||
|
go test ./internal/chart/v3/... # Chart v3 tests
|
||||||
|
make test-style # Linting
|
||||||
|
```
|
||||||
|
|
||||||
|
## bd Issues
|
||||||
|
|
||||||
|
| ID | Phase | Status |
|
||||||
|
|----|-------|--------|
|
||||||
|
| helm-btq | Phase 2: Chart Metadata | in-progress |
|
||||||
|
| helm-ed3 | Phase 3: DAG Construction | blocked-by helm-btq |
|
||||||
|
| helm-40n | Phase 4: WaitStrategy | future worktree |
|
||||||
|
| helm-fby | Phase 5: Action System | future worktree |
|
||||||
|
| helm-0d5 | Phase 6: CLI | future worktree |
|
||||||
|
| helm-7an | Phase 7: Release Storage | future worktree |
|
||||||
@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DAG is a directed acyclic graph for dependency resolution.
|
||||||
|
// Nodes represent subcharts or resource-groups. Edges represent
|
||||||
|
// "must be ready before" relationships.
|
||||||
|
type DAG struct {
|
||||||
|
nodes map[string]bool
|
||||||
|
edges map[string]map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDAG creates an empty DAG.
|
||||||
|
func NewDAG() *DAG {
|
||||||
|
return &DAG{
|
||||||
|
nodes: make(map[string]bool),
|
||||||
|
edges: make(map[string]map[string]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNode adds a node to the graph. Idempotent.
|
||||||
|
func (d *DAG) AddNode(name string) {
|
||||||
|
d.nodes[name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEdge adds a directed edge: "from" must be ready before "to" can start.
|
||||||
|
func (d *DAG) AddEdge(from, to string) error {
|
||||||
|
if from == to {
|
||||||
|
return fmt.Errorf("self-dependency detected: %q depends on itself", from)
|
||||||
|
}
|
||||||
|
d.AddNode(from)
|
||||||
|
d.AddNode(to)
|
||||||
|
if d.edges[to] == nil {
|
||||||
|
d.edges[to] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
d.edges[to][from] = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectCycles checks for circular dependencies using DFS.
|
||||||
|
func (d *DAG) DetectCycles() error {
|
||||||
|
const (
|
||||||
|
unvisited = 0
|
||||||
|
visiting = 1
|
||||||
|
visited = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
state := make(map[string]int, len(d.nodes))
|
||||||
|
path := make([]string, 0)
|
||||||
|
|
||||||
|
var visit func(node string) error
|
||||||
|
visit = func(node string) error {
|
||||||
|
state[node] = visiting
|
||||||
|
path = append(path, node)
|
||||||
|
|
||||||
|
for dep := range d.edges[node] {
|
||||||
|
switch state[dep] {
|
||||||
|
case visiting:
|
||||||
|
cycleStart := -1
|
||||||
|
for i, n := range path {
|
||||||
|
if n == dep {
|
||||||
|
cycleStart = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cycle := append(path[cycleStart:], dep)
|
||||||
|
return fmt.Errorf("circular dependency detected: %s", strings.Join(cycle, " -> "))
|
||||||
|
case unvisited:
|
||||||
|
if err := visit(dep); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path[:len(path)-1]
|
||||||
|
state[node] = visited
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sorted := d.sortedNodes()
|
||||||
|
for _, node := range sorted {
|
||||||
|
if state[node] == unvisited {
|
||||||
|
if err := visit(node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopologicalSort returns nodes in dependency order.
|
||||||
|
func (d *DAG) TopologicalSort() ([]string, error) {
|
||||||
|
if err := d.DetectCycles(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inDegree := make(map[string]int, len(d.nodes))
|
||||||
|
for node := range d.nodes {
|
||||||
|
inDegree[node] = len(d.edges[node])
|
||||||
|
}
|
||||||
|
|
||||||
|
var queue []string
|
||||||
|
for node, deg := range inDegree {
|
||||||
|
if deg == 0 {
|
||||||
|
queue = append(queue, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(queue)
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
for len(queue) > 0 {
|
||||||
|
node := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
result = append(result, node)
|
||||||
|
|
||||||
|
for downstream := range d.nodes {
|
||||||
|
if d.edges[downstream][node] {
|
||||||
|
inDegree[downstream]--
|
||||||
|
if inDegree[downstream] == 0 {
|
||||||
|
queue = append(queue, downstream)
|
||||||
|
sort.Strings(queue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batches returns nodes grouped by dependency level (topological layers).
|
||||||
|
func (d *DAG) Batches() ([][]string, error) {
|
||||||
|
if err := d.DetectCycles(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.nodes) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
inDegree := make(map[string]int, len(d.nodes))
|
||||||
|
for node := range d.nodes {
|
||||||
|
inDegree[node] = len(d.edges[node])
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := make(map[string]bool)
|
||||||
|
for node := range d.nodes {
|
||||||
|
remaining[node] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var batches [][]string
|
||||||
|
for len(remaining) > 0 {
|
||||||
|
var batch []string
|
||||||
|
for node := range remaining {
|
||||||
|
if inDegree[node] == 0 {
|
||||||
|
batch = append(batch, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(batch) == 0 {
|
||||||
|
return nil, fmt.Errorf("internal error: no nodes with in-degree 0 among remaining nodes")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(batch)
|
||||||
|
batches = append(batches, batch)
|
||||||
|
|
||||||
|
for _, node := range batch {
|
||||||
|
delete(remaining, node)
|
||||||
|
for downstream := range remaining {
|
||||||
|
if d.edges[downstream][node] {
|
||||||
|
inDegree[downstream]--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return batches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes returns all node names.
|
||||||
|
func (d *DAG) Nodes() []string {
|
||||||
|
return d.sortedNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DependsOn returns the set of nodes that the given node depends on.
|
||||||
|
func (d *DAG) DependsOn(node string) []string {
|
||||||
|
deps := d.edges[node]
|
||||||
|
result := make([]string, 0, len(deps))
|
||||||
|
for dep := range deps {
|
||||||
|
result = append(result, dep)
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependents returns the set of nodes that depend on the given node.
|
||||||
|
func (d *DAG) Dependents(node string) []string {
|
||||||
|
var result []string
|
||||||
|
for n, deps := range d.edges {
|
||||||
|
if deps[node] {
|
||||||
|
result = append(result, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNode returns true if the node exists.
|
||||||
|
func (d *DAG) HasNode(name string) bool {
|
||||||
|
return d.nodes[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of nodes.
|
||||||
|
func (d *DAG) Len() int {
|
||||||
|
return len(d.nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DAG) sortedNodes() []string {
|
||||||
|
result := make([]string, 0, len(d.nodes))
|
||||||
|
for n := range d.nodes {
|
||||||
|
result = append(result, n)
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/internal/chart/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AnnotationDependsOnSubcharts is the Chart.yaml annotation key for
|
||||||
|
// declaring subchart dependencies that must be deployed and ready
|
||||||
|
// before the current chart's resources are installed.
|
||||||
|
AnnotationDependsOnSubcharts = "helm.sh/depends-on/subcharts"
|
||||||
|
|
||||||
|
// AnnotationResourceGroup is the template annotation key for declaring
|
||||||
|
// the resource-group a resource belongs to.
|
||||||
|
AnnotationResourceGroup = "helm.sh/resource-group"
|
||||||
|
|
||||||
|
// AnnotationDependsOnResourceGroups is the template annotation key for
|
||||||
|
// declaring resource-group dependencies.
|
||||||
|
AnnotationDependsOnResourceGroups = "helm.sh/depends-on/resource-groups"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseDependsOnSubcharts extracts the list of subchart names from the
|
||||||
|
// helm.sh/depends-on/subcharts annotation in Chart.yaml metadata.
|
||||||
|
// Returns nil if the annotation is not present.
|
||||||
|
func ParseDependsOnSubcharts(md *chart.Metadata) ([]string, error) {
|
||||||
|
if md == nil || md.Annotations == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
raw, ok := md.Annotations[AnnotationDependsOnSubcharts]
|
||||||
|
if !ok || raw == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
if err := json.Unmarshal([]byte(raw), &names); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid %s annotation: %w", AnnotationDependsOnSubcharts, err)
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildSubchartDAG constructs a dependency DAG for subcharts of the given chart.
|
||||||
|
// It combines dependencies declared via:
|
||||||
|
// - Dependency.DependsOn field entries in Chart.yaml dependencies
|
||||||
|
// - Metadata.Annotations["helm.sh/depends-on/subcharts"] for the parent chart
|
||||||
|
//
|
||||||
|
// Returns the DAG and any validation error (cycles, unknown references).
|
||||||
|
func BuildSubchartDAG(c *chart.Chart) (*DAG, error) {
|
||||||
|
dag := NewDAG()
|
||||||
|
|
||||||
|
if c.Metadata == nil {
|
||||||
|
return dag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a lookup of known dependency names/aliases
|
||||||
|
depNames := make(map[string]bool)
|
||||||
|
for _, dep := range c.Metadata.Dependencies {
|
||||||
|
key := dep.Name
|
||||||
|
if dep.Alias != "" {
|
||||||
|
key = dep.Alias
|
||||||
|
}
|
||||||
|
depNames[key] = true
|
||||||
|
dag.AddNode(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the parent chart node
|
||||||
|
dag.AddNode(c.Metadata.Name)
|
||||||
|
|
||||||
|
// Process Dependency.DependsOn field entries
|
||||||
|
for _, dep := range c.Metadata.Dependencies {
|
||||||
|
key := dep.Name
|
||||||
|
if dep.Alias != "" {
|
||||||
|
key = dep.Alias
|
||||||
|
}
|
||||||
|
for _, upstream := range dep.DependsOn {
|
||||||
|
if !depNames[upstream] {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"dependency %q declares depends-on %q, but %q is not a known dependency",
|
||||||
|
key, upstream, upstream,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if err := dag.AddEdge(upstream, key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process helm.sh/depends-on/subcharts annotation on parent chart
|
||||||
|
annotationDeps, err := ParseDependsOnSubcharts(c.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, upstream := range annotationDeps {
|
||||||
|
if !depNames[upstream] {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"chart %q annotation %s references %q, but %q is not a known dependency",
|
||||||
|
c.Metadata.Name, AnnotationDependsOnSubcharts, upstream, upstream,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if err := dag.AddEdge(upstream, c.Metadata.Name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dag.DetectCycles(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dag, nil
|
||||||
|
}
|
||||||
@ -0,0 +1,260 @@
|
|||||||
|
/*
|
||||||
|
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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DAG is a directed acyclic graph for dependency resolution.
|
||||||
|
// Nodes represent subcharts or resource-groups. Edges represent
|
||||||
|
// "must be ready before" relationships.
|
||||||
|
type DAG struct {
|
||||||
|
// nodes is the set of all node names in the graph.
|
||||||
|
nodes map[string]bool
|
||||||
|
// edges maps a node to the set of nodes it depends on (upstream).
|
||||||
|
// If edges["app"] = {"db": true}, then "db" must be ready before "app".
|
||||||
|
edges map[string]map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDAG creates an empty DAG.
|
||||||
|
func NewDAG() *DAG {
|
||||||
|
return &DAG{
|
||||||
|
nodes: make(map[string]bool),
|
||||||
|
edges: make(map[string]map[string]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNode adds a node to the graph. Idempotent.
|
||||||
|
func (d *DAG) AddNode(name string) {
|
||||||
|
d.nodes[name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEdge adds a directed edge: "from" must be ready before "to" can start.
|
||||||
|
// Both nodes are implicitly added if not present.
|
||||||
|
// Returns an error if from == to (self-cycle).
|
||||||
|
func (d *DAG) AddEdge(from, to string) error {
|
||||||
|
if from == to {
|
||||||
|
return fmt.Errorf("self-dependency detected: %q depends on itself", from)
|
||||||
|
}
|
||||||
|
d.AddNode(from)
|
||||||
|
d.AddNode(to)
|
||||||
|
if d.edges[to] == nil {
|
||||||
|
d.edges[to] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
d.edges[to][from] = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectCycles checks for circular dependencies using DFS.
|
||||||
|
// Returns a descriptive error if a cycle is found, nil otherwise.
|
||||||
|
func (d *DAG) DetectCycles() error {
|
||||||
|
const (
|
||||||
|
unvisited = 0
|
||||||
|
visiting = 1
|
||||||
|
visited = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
state := make(map[string]int, len(d.nodes))
|
||||||
|
path := make([]string, 0)
|
||||||
|
|
||||||
|
var visit func(node string) error
|
||||||
|
visit = func(node string) error {
|
||||||
|
state[node] = visiting
|
||||||
|
path = append(path, node)
|
||||||
|
|
||||||
|
for dep := range d.edges[node] {
|
||||||
|
switch state[dep] {
|
||||||
|
case visiting:
|
||||||
|
// Found cycle — build cycle description
|
||||||
|
cycleStart := -1
|
||||||
|
for i, n := range path {
|
||||||
|
if n == dep {
|
||||||
|
cycleStart = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cycle := append(path[cycleStart:], dep)
|
||||||
|
return fmt.Errorf("circular dependency detected: %s", strings.Join(cycle, " -> "))
|
||||||
|
case unvisited:
|
||||||
|
if err := visit(dep); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path[:len(path)-1]
|
||||||
|
state[node] = visited
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit in sorted order for deterministic error messages
|
||||||
|
sorted := d.sortedNodes()
|
||||||
|
for _, node := range sorted {
|
||||||
|
if state[node] == unvisited {
|
||||||
|
if err := visit(node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopologicalSort returns nodes in dependency order: nodes with no
|
||||||
|
// dependencies come first. Nodes at the same level are sorted
|
||||||
|
// alphabetically for determinism.
|
||||||
|
func (d *DAG) TopologicalSort() ([]string, error) {
|
||||||
|
if err := d.DetectCycles(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inDegree := make(map[string]int, len(d.nodes))
|
||||||
|
for node := range d.nodes {
|
||||||
|
inDegree[node] = len(d.edges[node])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect nodes with no incoming edges
|
||||||
|
var queue []string
|
||||||
|
for node, deg := range inDegree {
|
||||||
|
if deg == 0 {
|
||||||
|
queue = append(queue, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(queue)
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
for len(queue) > 0 {
|
||||||
|
node := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
result = append(result, node)
|
||||||
|
|
||||||
|
// For each node that depends on this one, decrement in-degree
|
||||||
|
for downstream := range d.nodes {
|
||||||
|
if d.edges[downstream][node] {
|
||||||
|
inDegree[downstream]--
|
||||||
|
if inDegree[downstream] == 0 {
|
||||||
|
queue = append(queue, downstream)
|
||||||
|
sort.Strings(queue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batches returns nodes grouped by dependency level (topological layers).
|
||||||
|
// Each batch can be deployed in parallel; batches must be deployed sequentially.
|
||||||
|
// Batch 0 has no dependencies, batch 1 depends only on batch 0, etc.
|
||||||
|
func (d *DAG) Batches() ([][]string, error) {
|
||||||
|
if err := d.DetectCycles(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.nodes) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
inDegree := make(map[string]int, len(d.nodes))
|
||||||
|
for node := range d.nodes {
|
||||||
|
inDegree[node] = len(d.edges[node])
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := make(map[string]bool)
|
||||||
|
for node := range d.nodes {
|
||||||
|
remaining[node] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var batches [][]string
|
||||||
|
for len(remaining) > 0 {
|
||||||
|
// Find all nodes with in-degree 0 among remaining
|
||||||
|
var batch []string
|
||||||
|
for node := range remaining {
|
||||||
|
if inDegree[node] == 0 {
|
||||||
|
batch = append(batch, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(batch) == 0 {
|
||||||
|
// Should not happen after cycle check, but guard against it
|
||||||
|
return nil, fmt.Errorf("internal error: no nodes with in-degree 0 among remaining nodes")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(batch)
|
||||||
|
batches = append(batches, batch)
|
||||||
|
|
||||||
|
// Remove batch nodes and update in-degrees
|
||||||
|
for _, node := range batch {
|
||||||
|
delete(remaining, node)
|
||||||
|
for downstream := range remaining {
|
||||||
|
if d.edges[downstream][node] {
|
||||||
|
inDegree[downstream]--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return batches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes returns all node names in the DAG.
|
||||||
|
func (d *DAG) Nodes() []string {
|
||||||
|
return d.sortedNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DependsOn returns the set of nodes that the given node depends on.
|
||||||
|
func (d *DAG) DependsOn(node string) []string {
|
||||||
|
deps := d.edges[node]
|
||||||
|
result := make([]string, 0, len(deps))
|
||||||
|
for dep := range deps {
|
||||||
|
result = append(result, dep)
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependents returns the set of nodes that depend on the given node.
|
||||||
|
func (d *DAG) Dependents(node string) []string {
|
||||||
|
var result []string
|
||||||
|
for n, deps := range d.edges {
|
||||||
|
if deps[node] {
|
||||||
|
result = append(result, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNode returns true if the node exists in the graph.
|
||||||
|
func (d *DAG) HasNode(name string) bool {
|
||||||
|
return d.nodes[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of nodes.
|
||||||
|
func (d *DAG) Len() int {
|
||||||
|
return len(d.nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DAG) sortedNodes() []string {
|
||||||
|
result := make([]string, 0, len(d.nodes))
|
||||||
|
for n := range d.nodes {
|
||||||
|
result = append(result, n)
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDAGEmpty(t *testing.T) {
|
||||||
|
dag := NewDAG()
|
||||||
|
assert.Equal(t, 0, dag.Len())
|
||||||
|
|
||||||
|
sorted, err := dag.TopologicalSort()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, sorted)
|
||||||
|
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, batches)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGSingleNode(t *testing.T) {
|
||||||
|
dag := NewDAG()
|
||||||
|
dag.AddNode("nginx")
|
||||||
|
|
||||||
|
assert.Equal(t, 1, dag.Len())
|
||||||
|
assert.True(t, dag.HasNode("nginx"))
|
||||||
|
|
||||||
|
sorted, err := dag.TopologicalSort()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"nginx"}, sorted)
|
||||||
|
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, [][]string{{"nginx"}}, batches)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGLinearChain(t *testing.T) {
|
||||||
|
// A -> B -> C (C depends on B, B depends on A)
|
||||||
|
dag := NewDAG()
|
||||||
|
require.NoError(t, dag.AddEdge("A", "B"))
|
||||||
|
require.NoError(t, dag.AddEdge("B", "C"))
|
||||||
|
|
||||||
|
sorted, err := dag.TopologicalSort()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, []string{"A", "B", "C"}, sorted)
|
||||||
|
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, [][]string{{"A"}, {"B"}, {"C"}}, batches)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGDiamondDependency(t *testing.T) {
|
||||||
|
// HIP-0025 example: nginx + rabbitmq -> bar -> foo
|
||||||
|
dag := NewDAG()
|
||||||
|
require.NoError(t, dag.AddEdge("nginx", "bar"))
|
||||||
|
require.NoError(t, dag.AddEdge("rabbitmq", "bar"))
|
||||||
|
require.NoError(t, dag.AddEdge("bar", "foo"))
|
||||||
|
require.NoError(t, dag.AddEdge("rabbitmq", "foo"))
|
||||||
|
|
||||||
|
sorted, err := dag.TopologicalSort()
|
||||||
|
require.NoError(t, err)
|
||||||
|
// nginx and rabbitmq have no deps, come first (alphabetical)
|
||||||
|
assert.Equal(t, "nginx", sorted[0])
|
||||||
|
assert.Equal(t, "rabbitmq", sorted[1])
|
||||||
|
// bar depends on both, comes next
|
||||||
|
assert.Equal(t, "bar", sorted[2])
|
||||||
|
// foo depends on bar and rabbitmq
|
||||||
|
assert.Equal(t, "foo", sorted[3])
|
||||||
|
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
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 TestDAGParallelNoDeps(t *testing.T) {
|
||||||
|
dag := NewDAG()
|
||||||
|
dag.AddNode("a")
|
||||||
|
dag.AddNode("b")
|
||||||
|
dag.AddNode("c")
|
||||||
|
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, batches, 1)
|
||||||
|
assert.Equal(t, []string{"a", "b", "c"}, batches[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGCircularDependency(t *testing.T) {
|
||||||
|
dag := NewDAG()
|
||||||
|
require.NoError(t, dag.AddEdge("A", "B"))
|
||||||
|
require.NoError(t, dag.AddEdge("B", "C"))
|
||||||
|
require.NoError(t, dag.AddEdge("C", "A"))
|
||||||
|
|
||||||
|
err := dag.DetectCycles()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "circular dependency detected")
|
||||||
|
|
||||||
|
_, err = dag.TopologicalSort()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "circular dependency detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGSelfDependency(t *testing.T) {
|
||||||
|
dag := NewDAG()
|
||||||
|
err := dag.AddEdge("A", "A")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "self-dependency detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGDependsOnAndDependents(t *testing.T) {
|
||||||
|
dag := NewDAG()
|
||||||
|
require.NoError(t, dag.AddEdge("db", "app"))
|
||||||
|
require.NoError(t, dag.AddEdge("cache", "app"))
|
||||||
|
|
||||||
|
deps := dag.DependsOn("app")
|
||||||
|
assert.Equal(t, []string{"cache", "db"}, deps)
|
||||||
|
|
||||||
|
dependents := dag.Dependents("db")
|
||||||
|
assert.Equal(t, []string{"app"}, dependents)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGOrphanedNodes(t *testing.T) {
|
||||||
|
// Nodes with no edges should appear in first batch
|
||||||
|
dag := NewDAG()
|
||||||
|
dag.AddNode("orphan")
|
||||||
|
require.NoError(t, dag.AddEdge("A", "B"))
|
||||||
|
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, batches, 2)
|
||||||
|
// First batch: A and orphan (no deps)
|
||||||
|
assert.Equal(t, []string{"A", "orphan"}, batches[0])
|
||||||
|
assert.Equal(t, []string{"B"}, batches[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGComplexGraph(t *testing.T) {
|
||||||
|
// Complex HIP-0025 scenario:
|
||||||
|
// database and queue have no deps
|
||||||
|
// app depends on database and queue
|
||||||
|
// queue depends on another-group (missing, but we add it)
|
||||||
|
dag := NewDAG()
|
||||||
|
dag.AddNode("database")
|
||||||
|
dag.AddNode("queue")
|
||||||
|
dag.AddNode("another-group")
|
||||||
|
require.NoError(t, dag.AddEdge("another-group", "queue"))
|
||||||
|
require.NoError(t, dag.AddEdge("database", "app"))
|
||||||
|
require.NoError(t, dag.AddEdge("queue", "app"))
|
||||||
|
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, batches, 3)
|
||||||
|
assert.Equal(t, []string{"another-group", "database"}, batches[0])
|
||||||
|
assert.Equal(t, []string{"queue"}, batches[1])
|
||||||
|
assert.Equal(t, []string{"app"}, batches[2])
|
||||||
|
}
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AnnotationDependsOnSubcharts is the Chart.yaml annotation key for
|
||||||
|
// declaring subchart dependencies that must be deployed and ready
|
||||||
|
// before the current chart's resources are installed.
|
||||||
|
AnnotationDependsOnSubcharts = "helm.sh/depends-on/subcharts"
|
||||||
|
|
||||||
|
// AnnotationResourceGroup is the template annotation key for declaring
|
||||||
|
// the resource-group a resource belongs to.
|
||||||
|
AnnotationResourceGroup = "helm.sh/resource-group"
|
||||||
|
|
||||||
|
// AnnotationDependsOnResourceGroups is the template annotation key for
|
||||||
|
// declaring resource-group dependencies.
|
||||||
|
AnnotationDependsOnResourceGroups = "helm.sh/depends-on/resource-groups"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseDependsOnSubcharts extracts the list of subchart names from the
|
||||||
|
// helm.sh/depends-on/subcharts annotation in Chart.yaml metadata.
|
||||||
|
// Returns nil if the annotation is not present.
|
||||||
|
func ParseDependsOnSubcharts(md *chart.Metadata) ([]string, error) {
|
||||||
|
if md == nil || md.Annotations == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
raw, ok := md.Annotations[AnnotationDependsOnSubcharts]
|
||||||
|
if !ok || raw == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
if err := json.Unmarshal([]byte(raw), &names); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid %s annotation: %w", AnnotationDependsOnSubcharts, err)
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildSubchartDAG constructs a dependency DAG for subcharts of the given chart.
|
||||||
|
// It combines dependencies declared via:
|
||||||
|
// - Dependency.DependsOn field entries in Chart.yaml dependencies
|
||||||
|
// - Metadata.Annotations["helm.sh/depends-on/subcharts"] for the parent chart
|
||||||
|
//
|
||||||
|
// Returns the DAG and any validation error (cycles, unknown references).
|
||||||
|
func BuildSubchartDAG(c *chart.Chart) (*DAG, error) {
|
||||||
|
dag := NewDAG()
|
||||||
|
|
||||||
|
if c.Metadata == nil {
|
||||||
|
return dag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a lookup of known dependency names/aliases
|
||||||
|
depNames := make(map[string]bool)
|
||||||
|
for _, dep := range c.Metadata.Dependencies {
|
||||||
|
key := dep.Name
|
||||||
|
if dep.Alias != "" {
|
||||||
|
key = dep.Alias
|
||||||
|
}
|
||||||
|
depNames[key] = true
|
||||||
|
dag.AddNode(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the parent chart node
|
||||||
|
dag.AddNode(c.Metadata.Name)
|
||||||
|
|
||||||
|
// Process Dependency.DependsOn field entries
|
||||||
|
for _, dep := range c.Metadata.Dependencies {
|
||||||
|
key := dep.Name
|
||||||
|
if dep.Alias != "" {
|
||||||
|
key = dep.Alias
|
||||||
|
}
|
||||||
|
for _, upstream := range dep.DependsOn {
|
||||||
|
if !depNames[upstream] {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"dependency %q declares depends-on %q, but %q is not a known dependency",
|
||||||
|
key, upstream, upstream,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// upstream must be ready before key can be installed
|
||||||
|
if err := dag.AddEdge(upstream, key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process helm.sh/depends-on/subcharts annotation on parent chart
|
||||||
|
annotationDeps, err := ParseDependsOnSubcharts(c.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, upstream := range annotationDeps {
|
||||||
|
if !depNames[upstream] {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"chart %q annotation %s references %q, but %q is not a known dependency",
|
||||||
|
c.Metadata.Name, AnnotationDependsOnSubcharts, upstream, upstream,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// upstream must be ready before parent chart resources
|
||||||
|
if err := dag.AddEdge(upstream, c.Metadata.Name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate no cycles
|
||||||
|
if err := dag.DetectCycles(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dag, nil
|
||||||
|
}
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseDependsOnSubcharts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
metadata *chart.Metadata
|
||||||
|
expected []string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil metadata",
|
||||||
|
metadata: nil,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no annotations",
|
||||||
|
metadata: &chart.Metadata{},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no depends-on annotation",
|
||||||
|
metadata: &chart.Metadata{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"other": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid annotation",
|
||||||
|
metadata: &chart.Metadata{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
AnnotationDependsOnSubcharts: `["bar", "rabbitmq"]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{"bar", "rabbitmq"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty array",
|
||||||
|
metadata: &chart.Metadata{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
AnnotationDependsOnSubcharts: `[]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid JSON",
|
||||||
|
metadata: &chart.Metadata{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
AnnotationDependsOnSubcharts: `not-json`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string value",
|
||||||
|
metadata: &chart.Metadata{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
AnnotationDependsOnSubcharts: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := ParseDependsOnSubcharts(tt.metadata)
|
||||||
|
if tt.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSubchartDAG(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
chart *chart.Chart
|
||||||
|
expectedBatch [][]string
|
||||||
|
expectError bool
|
||||||
|
errorContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no dependencies",
|
||||||
|
chart: &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "myapp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedBatch: [][]string{{"myapp"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HIP-0025 example: nginx+rabbitmq -> bar -> foo",
|
||||||
|
chart: &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "foo",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
AnnotationDependsOnSubcharts: `["bar", "rabbitmq"]`,
|
||||||
|
},
|
||||||
|
Dependencies: []*chart.Dependency{
|
||||||
|
{Name: "nginx"},
|
||||||
|
{Name: "rabbitmq"},
|
||||||
|
{
|
||||||
|
Name: "bar",
|
||||||
|
DependsOn: []string{"nginx", "rabbitmq"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedBatch: [][]string{
|
||||||
|
{"nginx", "rabbitmq"},
|
||||||
|
{"bar"},
|
||||||
|
{"foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "depends-on with alias",
|
||||||
|
chart: &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "parent",
|
||||||
|
Dependencies: []*chart.Dependency{
|
||||||
|
{Name: "postgresql", Alias: "db"},
|
||||||
|
{
|
||||||
|
Name: "app",
|
||||||
|
DependsOn: []string{"db"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedBatch: [][]string{
|
||||||
|
{"db", "parent"},
|
||||||
|
{"app"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "circular dependency via DependsOn",
|
||||||
|
chart: &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "parent",
|
||||||
|
Dependencies: []*chart.Dependency{
|
||||||
|
{Name: "A", DependsOn: []string{"B"}},
|
||||||
|
{Name: "B", DependsOn: []string{"A"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "circular dependency detected",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown dependency reference",
|
||||||
|
chart: &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "parent",
|
||||||
|
Dependencies: []*chart.Dependency{
|
||||||
|
{Name: "app", DependsOn: []string{"nonexistent"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "not a known dependency",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown annotation reference",
|
||||||
|
chart: &chart.Chart{
|
||||||
|
Metadata: &chart.Metadata{
|
||||||
|
Name: "parent",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
AnnotationDependsOnSubcharts: `["ghost"]`,
|
||||||
|
},
|
||||||
|
Dependencies: []*chart.Dependency{
|
||||||
|
{Name: "app"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
errorContains: "not a known dependency",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dag, err := BuildSubchartDAG(tt.chart)
|
||||||
|
if tt.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
if tt.errorContains != "" {
|
||||||
|
assert.Contains(t, err.Error(), tt.errorContains)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
batches, err := dag.Batches()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expectedBatch, batches)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue