Skip to content

Commit 83563a9

Browse files
idsulikplumpy
authored andcommitted
fix(helm): Fix helm package installation order (#9693)
* fix(helm): Fix helm package installation order * fix copyright Signed-off-by: Suleiman Dibirov <[email protected]> * tests Signed-off-by: Suleiman Dibirov <[email protected]> * lint Signed-off-by: Suleiman Dibirov <[email protected]> * fixes Signed-off-by: Suleiman Dibirov <[email protected]> * fix linters Signed-off-by: Suleiman Dibirov <[email protected]> --------- Signed-off-by: Suleiman Dibirov <[email protected]>
1 parent 4178090 commit 83563a9

File tree

7 files changed

+650
-528
lines changed

7 files changed

+650
-528
lines changed
+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
Copyright 2025 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package helm
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
23+
)
24+
25+
// DependencyGraph represents a graph of helm release dependencies
26+
type DependencyGraph struct {
27+
graph map[string][]string
28+
releases []latest.HelmRelease
29+
hasDependencies bool
30+
}
31+
32+
// NewDependencyGraph creates a new DependencyGraph from a list of helm releases
33+
func NewDependencyGraph(releases []latest.HelmRelease) (*DependencyGraph, error) {
34+
graph := make(map[string][]string)
35+
releaseNames := make(map[string]bool)
36+
37+
for _, r := range releases {
38+
if _, exists := releaseNames[r.Name]; exists {
39+
return nil, fmt.Errorf("duplicate release name %s", r.Name)
40+
}
41+
releaseNames[r.Name] = true
42+
}
43+
44+
// Check for non-existent dependencies
45+
hasDependencies := false
46+
for _, r := range releases {
47+
for _, dep := range r.DependsOn {
48+
if !releaseNames[dep] {
49+
return nil, fmt.Errorf("release %s depends on non-existent release %s", r.Name, dep)
50+
}
51+
hasDependencies = true
52+
}
53+
graph[r.Name] = r.DependsOn
54+
}
55+
56+
g := &DependencyGraph{
57+
graph: graph,
58+
releases: releases,
59+
hasDependencies: hasDependencies,
60+
}
61+
62+
if err := g.hasCycles(); err != nil {
63+
return nil, err
64+
}
65+
66+
return g, nil
67+
}
68+
69+
// GetReleasesByLevel returns releases grouped by their dependency level while preserving
70+
// the original order within each level. Level 0 contains releases with no dependencies,
71+
// level 1 contains releases that depend only on level 0 releases, and so on.
72+
func (g *DependencyGraph) GetReleasesByLevel() (map[int][]string, error) {
73+
if len(g.releases) == 0 {
74+
// For empty releases, return empty map to avoid nil
75+
return map[int][]string{}, nil
76+
}
77+
78+
if !g.hasDependencies {
79+
// Fast path: if no dependencies, all releases are at level 0
80+
// Preserve original order from releases slice
81+
return map[int][]string{
82+
0: g.getNames(),
83+
}, nil
84+
}
85+
86+
order, err := g.calculateDeploymentOrder()
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
return g.groupReleasesByLevel(order), nil
92+
}
93+
94+
// hasCycles checks if there are any cycles in the dependency graph
95+
func (g *DependencyGraph) hasCycles() error {
96+
if !g.hasDependencies {
97+
return nil
98+
}
99+
100+
visited := make(map[string]bool)
101+
recStack := make(map[string]bool)
102+
103+
var checkCycle func(node string) error
104+
checkCycle = func(node string) error {
105+
if !visited[node] {
106+
visited[node] = true
107+
recStack[node] = true
108+
109+
for _, dep := range g.graph[node] {
110+
if !visited[dep] {
111+
if err := checkCycle(dep); err != nil {
112+
return err
113+
}
114+
} else if recStack[dep] {
115+
return fmt.Errorf("cycle detected involving release %q", node)
116+
}
117+
}
118+
}
119+
recStack[node] = false
120+
return nil
121+
}
122+
123+
for node := range g.graph {
124+
if !visited[node] {
125+
if err := checkCycle(node); err != nil {
126+
return err
127+
}
128+
}
129+
}
130+
return nil
131+
}
132+
133+
// getNames returns a slice of release names in their original order
134+
func (g *DependencyGraph) getNames() []string {
135+
names := make([]string, len(g.releases))
136+
for i, release := range g.releases {
137+
names[i] = release.Name
138+
}
139+
return names
140+
}
141+
142+
// calculateDeploymentOrder returns a topologically sorted list of releases,
143+
// ensuring that releases are deployed after their dependencies while maintaining
144+
// the original order where possible
145+
func (g *DependencyGraph) calculateDeploymentOrder() ([]string, error) {
146+
visited := make(map[string]bool)
147+
order := make([]string, 0, len(g.releases))
148+
149+
// Create a mapping of release name to its index in original order
150+
originalOrder := make(map[string]int, len(g.releases))
151+
for i, release := range g.releases {
152+
originalOrder[release.Name] = i
153+
}
154+
155+
var visit func(node string) error
156+
visit = func(node string) error {
157+
if visited[node] {
158+
return nil
159+
}
160+
visited[node] = true
161+
162+
// Sort dependencies based on original order
163+
deps := make([]string, len(g.graph[node]))
164+
copy(deps, g.graph[node])
165+
if len(deps) > 1 {
166+
// Sort dependencies by their original position
167+
for i := 0; i < len(deps)-1; i++ {
168+
for j := i + 1; j < len(deps); j++ {
169+
if originalOrder[deps[i]] > originalOrder[deps[j]] {
170+
deps[i], deps[j] = deps[j], deps[i]
171+
}
172+
}
173+
}
174+
}
175+
176+
// Visit dependencies in original order
177+
for _, dep := range deps {
178+
if err := visit(dep); err != nil {
179+
return err
180+
}
181+
}
182+
order = append(order, node)
183+
return nil
184+
}
185+
186+
// Process releases in their original order
187+
for _, release := range g.releases {
188+
if err := visit(release.Name); err != nil {
189+
return nil, err
190+
}
191+
}
192+
193+
return order, nil
194+
}
195+
196+
// groupReleasesByLevel groups releases by their dependency level while preserving
197+
// the original order within each level
198+
func (g *DependencyGraph) groupReleasesByLevel(order []string) map[int][]string {
199+
levels := make(map[int][]string)
200+
releaseLevels := make(map[string]int)
201+
202+
// Calculate level for each release
203+
for _, release := range order {
204+
level := 0
205+
for _, dep := range g.graph[release] {
206+
if depLevel, exists := releaseLevels[dep]; exists {
207+
if depLevel >= level {
208+
level = depLevel + 1
209+
}
210+
}
211+
}
212+
releaseLevels[release] = level
213+
if levels[level] == nil {
214+
levels[level] = make([]string, 0)
215+
}
216+
levels[level] = append(levels[level], release)
217+
}
218+
219+
return levels
220+
}

0 commit comments

Comments
 (0)