Skip to content

Commit

Permalink
feat(helm): Add helm dependencies support (#9624)
Browse files Browse the repository at this point in the history
* feat(helm): Add helm dependencies support
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
  • Loading branch information
idsulik authored Jan 14, 2025
1 parent 71b7c53 commit 0e322e0
Show file tree
Hide file tree
Showing 6 changed files with 603 additions and 48 deletions.
12 changes: 11 additions & 1 deletion docs-v2/content/en/schemas/v4beta12.json
Original file line number Diff line number Diff line change
Expand Up @@ -2375,6 +2375,15 @@
"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`.",
"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>."
},
"dependsOn": {
"items": {
"type": "string"
},
"type": "array",
"description": "a list of Helm release names that this deploy depends on.",
"x-intellij-html-description": "a list of Helm release names that this deploy depends on.",
"default": "[]"
},
"name": {
"type": "string",
"description": "name of the Helm release. It accepts environment variables via the go template syntax.",
Expand Down Expand Up @@ -2490,7 +2499,8 @@
"repo",
"upgradeOnChange",
"overrides",
"packaged"
"packaged",
"dependsOn"
],
"additionalProperties": false,
"type": "object",
Expand Down
116 changes: 72 additions & 44 deletions pkg/skaffold/deploy/helm/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,31 @@ func (h *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Art

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

// Build dependency graph to determine the order of Helm release deployments.
dependencyGraph, err := BuildDependencyGraph(h.Releases)
if err != nil {
return fmt.Errorf("error building dependency graph: %w", err)
}

// Verify no cycles in the dependency graph
if err := VerifyNoCycles(dependencyGraph); err != nil {
return fmt.Errorf("error verifying dependency graph: %w", err)
}

// Calculate deployment order
deploymentOrder, err := calculateDeploymentOrder(dependencyGraph)
if err != nil {
return fmt.Errorf("error calculating deployment order: %w", err)
}

var mu sync2.Mutex
nsMap := map[string]struct{}{}
manifests := manifest.ManifestList{}
g, ctx := errgroup.WithContext(ctx)

// Group releases by their dependency level to deploy them in the correct order.
levelGroups := groupReleasesByLevel(deploymentOrder, dependencyGraph)

g, levelCtx := errgroup.WithContext(ctx)

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

var mu sync2.Mutex
// Deploy every release
releaseNameToRelease := make(map[string]latest.HelmRelease)
for _, r := range h.Releases {
g.Go(func() error {
releaseName, err := util.ExpandEnvTemplateOrFail(r.Name, nil)
if err != nil {
return helm.UserErr(fmt.Sprintf("cannot expand release name %q", r.Name), err)
}
chartVersion, err := util.ExpandEnvTemplateOrFail(r.Version, nil)
if err != nil {
return helm.UserErr(fmt.Sprintf("cannot expand chart version %q", r.Version), err)
}
releaseName, err := util.ExpandEnvTemplateOrFail(r.Name, nil)
if err != nil {
return fmt.Errorf("cannot parse the release name template: %w", err)
}
releaseNameToRelease[releaseName] = r
}

repo, err := util.ExpandEnvTemplateOrFail(r.Repo, nil)
if err != nil {
return helm.UserErr(fmt.Sprintf("cannot expand repo %q", r.Repo), err)
}
r.ChartPath, err = util.ExpandEnvTemplateOrFail(r.ChartPath, nil)
if err != nil {
return helm.UserErr(fmt.Sprintf("cannot expand chart path %q", r.ChartPath), err)
}
// Process each level in order
for level, releases := range levelGroups {
if len(levelGroups) > 1 {
olog.Entry(ctx).Infof("Installing level %d/%d releases (%d releases)", level, len(levelGroups), len(releases))
} else {
olog.Entry(ctx).Infof("Installing releases (%d releases)", len(releases))
}

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

mu.Lock()
manifests.Append(m)
mu.Unlock()
g.Go(func() error {
chartVersion, err := util.ExpandEnvTemplateOrFail(release.Version, nil)
if err != nil {
return helm.UserErr(fmt.Sprintf("cannot expand chart version %q", release.Version), err)
}

// Collect namespaces first
newNamespaces := make(map[string]struct{})
for _, res := range results {
if trimmed := strings.TrimSpace(res.Namespace); trimmed != "" {
newNamespaces[trimmed] = struct{}{}
repo, err := util.ExpandEnvTemplateOrFail(release.Repo, nil)
if err != nil {
return helm.UserErr(fmt.Sprintf("cannot expand repo %q", release.Repo), err)
}
}

// Lock only once to update nsMap
mu.Lock()
for ns := range newNamespaces {
nsMap[ns] = struct{}{}
}
mu.Unlock()
release.ChartPath, err = util.ExpandEnvTemplateOrFail(release.ChartPath, nil)
if err != nil {
return helm.UserErr(fmt.Sprintf("cannot expand chart path %q", release.ChartPath), err)
}

return nil
})
}
m, results, err := h.deployRelease(levelCtx, out, releaseName, release, builds, h.bV, chartVersion, repo)
if err != nil {
return helm.UserErr(fmt.Sprintf("deploying %q", releaseName), err)
}

if err := g.Wait(); err != nil {
return err
mu.Lock()
defer mu.Unlock()
manifests.Append(m)
for _, res := range results {
if trimmed := strings.TrimSpace(res.Namespace); trimmed != "" {
nsMap[trimmed] = struct{}{}
}
}

return nil
})
}

if err := g.Wait(); err != nil {
return err
}
}

// Let's make sure that every image tag is set with `--set`.
Expand Down
132 changes: 132 additions & 0 deletions pkg/skaffold/deploy/helm/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
Copyright 2019 The Skaffold 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 helm

import (
"fmt"

"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/helm"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util"
)

func BuildDependencyGraph(releases []latest.HelmRelease) (map[string][]string, error) {
dependencyGraph := make(map[string][]string)
for _, r := range releases {
releaseName, err := util.ExpandEnvTemplateOrFail(r.Name, nil)
if err != nil {
return nil, helm.UserErr(fmt.Sprintf("cannot expand release name %q", r.Name), err)
}
dependencyGraph[releaseName] = r.DependsOn
}

return dependencyGraph, nil
}

// VerifyNoCycles checks if there are any cycles in the dependency graph
func VerifyNoCycles(graph map[string][]string) error {
visited := make(map[string]bool)
recStack := make(map[string]bool)

var checkCycle func(node string) error
checkCycle = func(node string) error {
if !visited[node] {
visited[node] = true
recStack[node] = true

for _, dep := range graph[node] {
if !visited[dep] {
if err := checkCycle(dep); err != nil {
return err
}
} else if recStack[dep] {
return fmt.Errorf("cycle detected involving release %s", node)
}
}
}
recStack[node] = false
return nil
}

for node := range graph {
if !visited[node] {
if err := checkCycle(node); err != nil {
return err
}
}
}
return nil
}

// calculateDeploymentOrder returns a topologically sorted list of releases,
// ensuring that releases are deployed after their dependencies.
func calculateDeploymentOrder(graph map[string][]string) ([]string, error) {
visited := make(map[string]bool)
order := make([]string, 0)

var visit func(node string) error
visit = func(node string) error {
if visited[node] {
return nil
}
visited[node] = true

for _, dep := range graph[node] {
if err := visit(dep); err != nil {
return err
}
}
order = append(order, node)
return nil
}

for node := range graph {
if err := visit(node); err != nil {
return nil, err
}
}

return order, nil
}

// groupReleasesByLevel groups releases by their dependency level
// Level 0 contains releases with no dependencies
// Level 1 contains releases that depend only on level 0 releases
// And so on...
func groupReleasesByLevel(order []string, graph map[string][]string) map[int][]string {
levels := make(map[int][]string)
releaseLevels := make(map[string]int)

// Calculate level for each release
for _, release := range order {
level := 0
for _, dep := range graph[release] {
if depLevel, exists := releaseLevels[dep]; exists {
if depLevel >= level {
level = depLevel + 1
}
}
}
releaseLevels[release] = level
if levels[level] == nil {
levels[level] = make([]string, 0)
}
levels[level] = append(levels[level], release)
}

return levels
}
Loading

0 comments on commit 0e322e0

Please sign in to comment.