Skip to content

Commit 0e322e0

Browse files
authored
feat(helm): Add helm dependencies support (#9624)
* feat(helm): Add helm dependencies support Signed-off-by: Suleiman Dibirov <[email protected]>
1 parent 71b7c53 commit 0e322e0

File tree

6 files changed

+603
-48
lines changed

6 files changed

+603
-48
lines changed

docs-v2/content/en/schemas/v4beta12.json

+11-1
Original file line numberDiff line numberDiff line change
@@ -2375,6 +2375,15 @@
23752375
"description": "if `true`, Skaffold will send `--create-namespace` flag to Helm CLI. `--create-namespace` flag is available in Helm since version 3.2. Defaults is `false`.",
23762376
"x-intellij-html-description": "if <code>true</code>, Skaffold will send <code>--create-namespace</code> flag to Helm CLI. <code>--create-namespace</code> flag is available in Helm since version 3.2. Defaults is <code>false</code>."
23772377
},
2378+
"dependsOn": {
2379+
"items": {
2380+
"type": "string"
2381+
},
2382+
"type": "array",
2383+
"description": "a list of Helm release names that this deploy depends on.",
2384+
"x-intellij-html-description": "a list of Helm release names that this deploy depends on.",
2385+
"default": "[]"
2386+
},
23782387
"name": {
23792388
"type": "string",
23802389
"description": "name of the Helm release. It accepts environment variables via the go template syntax.",
@@ -2490,7 +2499,8 @@
24902499
"repo",
24912500
"upgradeOnChange",
24922501
"overrides",
2493-
"packaged"
2502+
"packaged",
2503+
"dependsOn"
24942504
],
24952505
"additionalProperties": false,
24962506
"type": "object",

pkg/skaffold/deploy/helm/helm.go

+72-44
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,31 @@ func (h *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Art
261261

262262
olog.Entry(ctx).Infof("Deploying with helm v%s ...", h.bV)
263263

264+
// Build dependency graph to determine the order of Helm release deployments.
265+
dependencyGraph, err := BuildDependencyGraph(h.Releases)
266+
if err != nil {
267+
return fmt.Errorf("error building dependency graph: %w", err)
268+
}
269+
270+
// Verify no cycles in the dependency graph
271+
if err := VerifyNoCycles(dependencyGraph); err != nil {
272+
return fmt.Errorf("error verifying dependency graph: %w", err)
273+
}
274+
275+
// Calculate deployment order
276+
deploymentOrder, err := calculateDeploymentOrder(dependencyGraph)
277+
if err != nil {
278+
return fmt.Errorf("error calculating deployment order: %w", err)
279+
}
280+
281+
var mu sync2.Mutex
264282
nsMap := map[string]struct{}{}
265283
manifests := manifest.ManifestList{}
266-
g, ctx := errgroup.WithContext(ctx)
284+
285+
// Group releases by their dependency level to deploy them in the correct order.
286+
levelGroups := groupReleasesByLevel(deploymentOrder, dependencyGraph)
287+
288+
g, levelCtx := errgroup.WithContext(ctx)
267289

268290
if h.Concurrency == nil || *h.Concurrency == 1 {
269291
g.SetLimit(1)
@@ -273,58 +295,64 @@ func (h *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Art
273295
olog.Entry(ctx).Infof("Installing %d releases concurrently", len(h.Releases))
274296
}
275297

276-
var mu sync2.Mutex
277-
// Deploy every release
298+
releaseNameToRelease := make(map[string]latest.HelmRelease)
278299
for _, r := range h.Releases {
279-
g.Go(func() error {
280-
releaseName, err := util.ExpandEnvTemplateOrFail(r.Name, nil)
281-
if err != nil {
282-
return helm.UserErr(fmt.Sprintf("cannot expand release name %q", r.Name), err)
283-
}
284-
chartVersion, err := util.ExpandEnvTemplateOrFail(r.Version, nil)
285-
if err != nil {
286-
return helm.UserErr(fmt.Sprintf("cannot expand chart version %q", r.Version), err)
287-
}
300+
releaseName, err := util.ExpandEnvTemplateOrFail(r.Name, nil)
301+
if err != nil {
302+
return fmt.Errorf("cannot parse the release name template: %w", err)
303+
}
304+
releaseNameToRelease[releaseName] = r
305+
}
288306

289-
repo, err := util.ExpandEnvTemplateOrFail(r.Repo, nil)
290-
if err != nil {
291-
return helm.UserErr(fmt.Sprintf("cannot expand repo %q", r.Repo), err)
292-
}
293-
r.ChartPath, err = util.ExpandEnvTemplateOrFail(r.ChartPath, nil)
294-
if err != nil {
295-
return helm.UserErr(fmt.Sprintf("cannot expand chart path %q", r.ChartPath), err)
296-
}
307+
// Process each level in order
308+
for level, releases := range levelGroups {
309+
if len(levelGroups) > 1 {
310+
olog.Entry(ctx).Infof("Installing level %d/%d releases (%d releases)", level, len(levelGroups), len(releases))
311+
} else {
312+
olog.Entry(ctx).Infof("Installing releases (%d releases)", len(releases))
313+
}
297314

298-
m, results, err := h.deployRelease(ctx, out, releaseName, r, builds, h.bV, chartVersion, repo)
299-
if err != nil {
300-
return helm.UserErr(fmt.Sprintf("deploying %q", releaseName), err)
301-
}
315+
// Deploy releases in current level
316+
for _, releaseName := range releases {
317+
release := releaseNameToRelease[releaseName]
302318

303-
mu.Lock()
304-
manifests.Append(m)
305-
mu.Unlock()
319+
g.Go(func() error {
320+
chartVersion, err := util.ExpandEnvTemplateOrFail(release.Version, nil)
321+
if err != nil {
322+
return helm.UserErr(fmt.Sprintf("cannot expand chart version %q", release.Version), err)
323+
}
306324

307-
// Collect namespaces first
308-
newNamespaces := make(map[string]struct{})
309-
for _, res := range results {
310-
if trimmed := strings.TrimSpace(res.Namespace); trimmed != "" {
311-
newNamespaces[trimmed] = struct{}{}
325+
repo, err := util.ExpandEnvTemplateOrFail(release.Repo, nil)
326+
if err != nil {
327+
return helm.UserErr(fmt.Sprintf("cannot expand repo %q", release.Repo), err)
312328
}
313-
}
314329

315-
// Lock only once to update nsMap
316-
mu.Lock()
317-
for ns := range newNamespaces {
318-
nsMap[ns] = struct{}{}
319-
}
320-
mu.Unlock()
330+
release.ChartPath, err = util.ExpandEnvTemplateOrFail(release.ChartPath, nil)
331+
if err != nil {
332+
return helm.UserErr(fmt.Sprintf("cannot expand chart path %q", release.ChartPath), err)
333+
}
321334

322-
return nil
323-
})
324-
}
335+
m, results, err := h.deployRelease(levelCtx, out, releaseName, release, builds, h.bV, chartVersion, repo)
336+
if err != nil {
337+
return helm.UserErr(fmt.Sprintf("deploying %q", releaseName), err)
338+
}
325339

326-
if err := g.Wait(); err != nil {
327-
return err
340+
mu.Lock()
341+
defer mu.Unlock()
342+
manifests.Append(m)
343+
for _, res := range results {
344+
if trimmed := strings.TrimSpace(res.Namespace); trimmed != "" {
345+
nsMap[trimmed] = struct{}{}
346+
}
347+
}
348+
349+
return nil
350+
})
351+
}
352+
353+
if err := g.Wait(); err != nil {
354+
return err
355+
}
328356
}
329357

330358
// Let's make sure that every image tag is set with `--set`.

pkg/skaffold/deploy/helm/util.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
Copyright 2019 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/helm"
23+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
24+
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util"
25+
)
26+
27+
func BuildDependencyGraph(releases []latest.HelmRelease) (map[string][]string, error) {
28+
dependencyGraph := make(map[string][]string)
29+
for _, r := range releases {
30+
releaseName, err := util.ExpandEnvTemplateOrFail(r.Name, nil)
31+
if err != nil {
32+
return nil, helm.UserErr(fmt.Sprintf("cannot expand release name %q", r.Name), err)
33+
}
34+
dependencyGraph[releaseName] = r.DependsOn
35+
}
36+
37+
return dependencyGraph, nil
38+
}
39+
40+
// VerifyNoCycles checks if there are any cycles in the dependency graph
41+
func VerifyNoCycles(graph map[string][]string) error {
42+
visited := make(map[string]bool)
43+
recStack := make(map[string]bool)
44+
45+
var checkCycle func(node string) error
46+
checkCycle = func(node string) error {
47+
if !visited[node] {
48+
visited[node] = true
49+
recStack[node] = true
50+
51+
for _, dep := range graph[node] {
52+
if !visited[dep] {
53+
if err := checkCycle(dep); err != nil {
54+
return err
55+
}
56+
} else if recStack[dep] {
57+
return fmt.Errorf("cycle detected involving release %s", node)
58+
}
59+
}
60+
}
61+
recStack[node] = false
62+
return nil
63+
}
64+
65+
for node := range graph {
66+
if !visited[node] {
67+
if err := checkCycle(node); err != nil {
68+
return err
69+
}
70+
}
71+
}
72+
return nil
73+
}
74+
75+
// calculateDeploymentOrder returns a topologically sorted list of releases,
76+
// ensuring that releases are deployed after their dependencies.
77+
func calculateDeploymentOrder(graph map[string][]string) ([]string, error) {
78+
visited := make(map[string]bool)
79+
order := make([]string, 0)
80+
81+
var visit func(node string) error
82+
visit = func(node string) error {
83+
if visited[node] {
84+
return nil
85+
}
86+
visited[node] = true
87+
88+
for _, dep := range graph[node] {
89+
if err := visit(dep); err != nil {
90+
return err
91+
}
92+
}
93+
order = append(order, node)
94+
return nil
95+
}
96+
97+
for node := range graph {
98+
if err := visit(node); err != nil {
99+
return nil, err
100+
}
101+
}
102+
103+
return order, nil
104+
}
105+
106+
// groupReleasesByLevel groups releases by their dependency level
107+
// Level 0 contains releases with no dependencies
108+
// Level 1 contains releases that depend only on level 0 releases
109+
// And so on...
110+
func groupReleasesByLevel(order []string, graph map[string][]string) map[int][]string {
111+
levels := make(map[int][]string)
112+
releaseLevels := make(map[string]int)
113+
114+
// Calculate level for each release
115+
for _, release := range order {
116+
level := 0
117+
for _, dep := range graph[release] {
118+
if depLevel, exists := releaseLevels[dep]; exists {
119+
if depLevel >= level {
120+
level = depLevel + 1
121+
}
122+
}
123+
}
124+
releaseLevels[release] = level
125+
if levels[level] == nil {
126+
levels[level] = make([]string, 0)
127+
}
128+
levels[level] = append(levels[level], release)
129+
}
130+
131+
return levels
132+
}

0 commit comments

Comments
 (0)