mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 12:05:19 +02:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
042df585b8 | ||
|
|
6ccfe559b1 | ||
|
|
bfcabf3ead | ||
|
|
3dda0385e5 | ||
|
|
d3cfe04d47 | ||
|
|
08504ad6f6 |
@@ -285,6 +285,24 @@ There's also `releases[].jsonPatches` that works similarly to `strategicMergePat
|
||||
|
||||
Please also see [test/advanced/helmfile.yaml](https://github.com/helmfile/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of patching support and more.
|
||||
|
||||
#### Patching envelope charts (charts whose subcharts have local dependencies)
|
||||
|
||||
`strategicMergePatches`, `jsonPatches`, and `transformers` all work on resources defined
|
||||
anywhere in the chart, including resources rendered by transitive subcharts (a subchart's
|
||||
own dependencies).
|
||||
|
||||
For local envelope charts whose subcharts declare their own `file://` dependencies, Helmfile
|
||||
runs `helm dependency build` bottom-up across the transitive dependency tree before chartify
|
||||
runs. Without this, helm's `dependency build` on the parent chart would fetch each file://
|
||||
subchart and package it as a `.tgz` **without** recursively building the subchart's own deps
|
||||
first. The resulting `.tgz` would lack the nested subchart's resources, chartify's
|
||||
`helm template` would silently drop them, and patches targeting those resources would fail
|
||||
with `no resource matches`.
|
||||
See [issue #851](https://github.com/helmfile/helmfile/issues/851).
|
||||
|
||||
To opt out of dependency building (e.g. for read-only `template` invocations), use
|
||||
`--skip-deps` or set `helmDefaults.skipDeps: true` / `releases[].skipDeps: true`.
|
||||
|
||||
### `transformers`
|
||||
|
||||
You can set `transformers` to apply [Kustomize's transformers](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead).
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
)
|
||||
|
||||
// skipIfNoHelm skips the calling test when the helm binary is not available on
|
||||
// PATH. Tests that exercise runHelmDepBuild through a real helmexec need helm
|
||||
// installed to produce meaningful results.
|
||||
func skipIfNoHelm(t *testing.T) {
|
||||
t.Helper()
|
||||
if _, err := exec.LookPath("helm"); err != nil {
|
||||
t.Skipf("skipping: helm binary not found in PATH (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// newRealHelmExec builds a helmexec.Interface backed by the real helm binary on
|
||||
// PATH, for tests that assert actual `helm dependency build` side effects.
|
||||
func newRealHelmExec(t *testing.T) helmexec.Interface {
|
||||
t.Helper()
|
||||
logger := zap.NewNop().Sugar()
|
||||
runner := &helmexec.ShellRunner{Ctx: context.Background(), Logger: logger}
|
||||
helm, err := helmexec.New("helm", helmexec.HelmExecOptions{}, logger, "", "", runner)
|
||||
if err != nil {
|
||||
t.Fatalf("helmexec.New: %v", err)
|
||||
}
|
||||
return helm
|
||||
}
|
||||
|
||||
// stubHelm is a minimal helmexec.Interface for prebuild tests that should NOT
|
||||
// invoke a real helm binary (avoids network I/O and the helm dependency). It
|
||||
// records BuildDeps/UpdateDeps calls so tests can assert which charts were
|
||||
// visited. Only BuildDeps/UpdateDeps are exercised by the prebuild path, so the
|
||||
// embedded nil Interface satisfies the rest of the contract without being
|
||||
// dereferenced.
|
||||
type stubHelm struct {
|
||||
helmexec.Interface
|
||||
buildErr error
|
||||
builds []string
|
||||
updates []string
|
||||
}
|
||||
|
||||
func (s *stubHelm) BuildDeps(name, chart string, flags ...string) error {
|
||||
s.builds = append(s.builds, chart)
|
||||
return s.buildErr
|
||||
}
|
||||
|
||||
func (s *stubHelm) UpdateDeps(chart string) error {
|
||||
s.updates = append(s.updates, chart)
|
||||
return s.buildErr
|
||||
}
|
||||
|
||||
// writeChart creates a chart directory with Chart.yaml and the given templates.
|
||||
// deps maps dependency name → repository URL (skipped if empty).
|
||||
func writeChart(t *testing.T, dir, name string, deps map[string]string, templates map[string]string) {
|
||||
t.Helper()
|
||||
chartYaml := "apiVersion: v2\nname: " + name + "\nversion: 0.1.0\n"
|
||||
if len(deps) > 0 {
|
||||
chartYaml += "dependencies:\n"
|
||||
for depName, repo := range deps {
|
||||
chartYaml += " - name: " + depName + "\n repository: " + repo + "\n version: 0.1.0\n"
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", dir, err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
||||
t.Fatalf("write Chart.yaml in %s: %v", dir, err)
|
||||
}
|
||||
if len(templates) > 0 {
|
||||
tmplDir := filepath.Join(dir, "templates")
|
||||
if err := os.MkdirAll(tmplDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", tmplDir, err)
|
||||
}
|
||||
for filename, content := range templates {
|
||||
if err := os.WriteFile(filepath.Join(tmplDir, filename), []byte(content), 0644); err != nil {
|
||||
t.Fatalf("write %s: %v", filename, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPreBuildTransitiveSubchartDeps_VisitsAllTransitiveDeps verifies that
|
||||
// preBuildTransitiveSubchartDeps walks the full transitive file:// dependency
|
||||
// tree by checking that helm dep build runs on every chart dir.
|
||||
//
|
||||
// We assert this by checking that sub2/charts/nested-* exists after the call —
|
||||
// that's the exact artifact that helm's non-recursive dep build fails to
|
||||
// produce for envelope charts (issue #851).
|
||||
func TestPreBuildTransitiveSubchartDeps_VisitsAllTransitiveDeps(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping in short mode (requires helm)")
|
||||
}
|
||||
skipIfNoHelm(t)
|
||||
|
||||
rootDir, err := os.MkdirTemp("", "helmfile-prebuild-")
|
||||
if err != nil {
|
||||
t.Fatalf("mkdtemp: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
// envelope → sub1, sub2
|
||||
// sub2 → nested
|
||||
writeChart(t, filepath.Join(rootDir, "envelope"), "envelope",
|
||||
map[string]string{"sub1": "file://../sub1", "sub2": "file://../sub2"},
|
||||
map[string]string{"cm.yaml": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: parent-cm\n"},
|
||||
)
|
||||
writeChart(t, filepath.Join(rootDir, "sub1"), "sub1", nil,
|
||||
map[string]string{"sm.yaml": "apiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n name: sub1-sm\n"},
|
||||
)
|
||||
writeChart(t, filepath.Join(rootDir, "sub2"), "sub2",
|
||||
map[string]string{"nested": "file://../nested"},
|
||||
map[string]string{"sm.yaml": "apiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n name: sub2-sm\n"},
|
||||
)
|
||||
writeChart(t, filepath.Join(rootDir, "nested"), "nested", nil,
|
||||
map[string]string{"sm.yaml": "apiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n name: nested-sm\n"},
|
||||
)
|
||||
|
||||
st := &HelmState{
|
||||
logger: zap.NewNop().Sugar(),
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
ReleaseSetSpec: ReleaseSetSpec{
|
||||
DefaultHelmBinary: "helm",
|
||||
},
|
||||
}
|
||||
|
||||
st.preBuildTransitiveSubchartDeps(filepath.Join(rootDir, "envelope"), newRealHelmExec(t))
|
||||
|
||||
// The critical assertion: sub2/charts/ should now contain nested (as
|
||||
// .tgz or unpacked). Without the fix, helm dep build on the parent chart
|
||||
// produces sub2.tgz WITHOUT nested, and the nested ServiceMonitor is
|
||||
// silently dropped from the chartify output.
|
||||
sub2Charts := filepath.Join(rootDir, "sub2", "charts")
|
||||
entries, err := os.ReadDir(sub2Charts)
|
||||
if err != nil {
|
||||
t.Fatalf("sub2/charts should exist after preBuildTransitiveSubchartDeps ran helm dep build on sub2: %v", err)
|
||||
}
|
||||
foundNested := false
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if name == "nested" || hasNestedTgzPrefix(name) {
|
||||
foundNested = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundNested {
|
||||
names := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
t.Fatalf("sub2/charts should contain nested after pre-build; got entries: %v", names)
|
||||
}
|
||||
|
||||
// envelope/charts should contain sub1 and sub2.
|
||||
envCharts := filepath.Join(rootDir, "envelope", "charts")
|
||||
if entries, err := os.ReadDir(envCharts); err != nil {
|
||||
t.Fatalf("envelope/charts should exist after pre-build: %v", err)
|
||||
} else if len(entries) == 0 {
|
||||
t.Fatalf("envelope/charts should contain sub1 and sub2 after pre-build")
|
||||
}
|
||||
}
|
||||
|
||||
func hasNestedTgzPrefix(name string) bool {
|
||||
return strings.HasPrefix(name, "nested-")
|
||||
}
|
||||
|
||||
// TestPreBuildTransitiveSubchartDeps_HandlesMissingChartYaml verifies the
|
||||
// function is a no-op (no panic) when the chart dir has no Chart.yaml. A stub
|
||||
// helm is used since the missing Chart.yaml short-circuits before any helm call.
|
||||
func TestPreBuildTransitiveSubchartDeps_HandlesMissingChartYaml(t *testing.T) {
|
||||
rootDir, err := os.MkdirTemp("", "helmfile-prebuild-noyaml-")
|
||||
if err != nil {
|
||||
t.Fatalf("mkdtemp: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
st := &HelmState{
|
||||
logger: zap.NewNop().Sugar(),
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
ReleaseSetSpec: ReleaseSetSpec{
|
||||
DefaultHelmBinary: "helm",
|
||||
},
|
||||
}
|
||||
|
||||
// Should not panic.
|
||||
st.preBuildTransitiveSubchartDeps(rootDir, &stubHelm{})
|
||||
}
|
||||
|
||||
// TestPreBuildTransitiveSubchartDeps_HandlesCircularDeps verifies the function
|
||||
// terminates when the file:// dependency graph has a cycle.
|
||||
func TestPreBuildTransitiveSubchartDeps_HandlesCircularDeps(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping in short mode (requires helm)")
|
||||
}
|
||||
skipIfNoHelm(t)
|
||||
|
||||
rootDir, err := os.MkdirTemp("", "helmfile-prebuild-cycle-")
|
||||
if err != nil {
|
||||
t.Fatalf("mkdtemp: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
// a → b → a (cycle). Use absolute file:// paths so each can resolve the other.
|
||||
aDir := filepath.Join(rootDir, "a")
|
||||
bDir := filepath.Join(rootDir, "b")
|
||||
writeChart(t, aDir, "a", map[string]string{"b": "file://" + bDir}, nil)
|
||||
writeChart(t, bDir, "b", map[string]string{"a": "file://" + aDir}, nil)
|
||||
|
||||
st := &HelmState{
|
||||
logger: zap.NewNop().Sugar(),
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
ReleaseSetSpec: ReleaseSetSpec{
|
||||
DefaultHelmBinary: "helm",
|
||||
},
|
||||
}
|
||||
|
||||
// Should terminate, not infinite-loop.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
st.preBuildTransitiveSubchartDeps(aDir, newRealHelmExec(t))
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
// success — terminated
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Fatal("preBuildTransitiveSubchartDeps did not terminate on circular deps")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPreBuildTransitiveSubchartDeps_DoesNotRecurseIntoRemoteDeps verifies the
|
||||
// function does not recurse into dependencies declared with non-file://
|
||||
// repositories (https://, oci://). It still runs helm dep build on the chart dir
|
||||
// itself, but does not follow the remote dependency.
|
||||
//
|
||||
// A stub helm whose BuildDeps returns an error is used so the test fails fast
|
||||
// and deterministically without performing any network I/O (a real helm would
|
||||
// try to fetch https://charts.example.com).
|
||||
func TestPreBuildTransitiveSubchartDeps_DoesNotRecurseIntoRemoteDeps(t *testing.T) {
|
||||
rootDir, err := os.MkdirTemp("", "helmfile-prebuild-remote-")
|
||||
if err != nil {
|
||||
t.Fatalf("mkdtemp: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
// Chart with only a remote dep. preBuildTransitiveSubchartDeps should not
|
||||
// recurse into anything (no file:// deps to follow).
|
||||
chartDir := filepath.Join(rootDir, "chart")
|
||||
writeChart(t, chartDir, "chart",
|
||||
map[string]string{"remote": "https://charts.example.com"},
|
||||
nil,
|
||||
)
|
||||
|
||||
st := &HelmState{
|
||||
logger: zap.NewNop().Sugar(),
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
ReleaseSetSpec: ReleaseSetSpec{
|
||||
DefaultHelmBinary: "helm",
|
||||
},
|
||||
}
|
||||
|
||||
helm := &stubHelm{buildErr: errors.New("stub: dependency build unavailable")}
|
||||
st.preBuildTransitiveSubchartDeps(chartDir, helm)
|
||||
|
||||
// BuildDeps must be called exactly once, on the chart dir itself — proving
|
||||
// the function did not recurse into the remote dep.
|
||||
if len(helm.builds) != 1 {
|
||||
t.Fatalf("expected exactly one BuildDeps call (chart dir, no recursion into remote), got %v", helm.builds)
|
||||
}
|
||||
if got, want := filepath.Base(helm.builds[0]), "chart"; got != want {
|
||||
t.Fatalf("BuildDeps called on %q, want chart dir %q", helm.builds[0], want)
|
||||
}
|
||||
// The stub error is not a lock-out-of-sync signal, so there must be no
|
||||
// fallback to UpdateDeps.
|
||||
if len(helm.updates) != 0 {
|
||||
t.Fatalf("expected no UpdateDeps fallback, got %v", helm.updates)
|
||||
}
|
||||
}
|
||||
+159
-2
@@ -1833,11 +1833,168 @@ func (st *HelmState) rewriteChartDependencies(chartPath string) (string, func(),
|
||||
return tempDir, cleanup, nil
|
||||
}
|
||||
|
||||
// preBuildTransitiveSubchartDeps walks the transitive tree of local file://
|
||||
// dependencies declared in chartPath/Chart.yaml and runs `helm dependency build`
|
||||
// on each chart's source directory bottom-up (dependencies before dependents).
|
||||
//
|
||||
// This fixes envelope charts whose subcharts declare their own file:// deps:
|
||||
// helm's `dependency build` on the parent packages each subchart as .tgz without
|
||||
// recursively building the subchart's own deps, so the nested subchart's
|
||||
// resources are silently absent and patches targeting them fail with
|
||||
// "no resource matches". See https://github.com/helmfile/helmfile/issues/851.
|
||||
//
|
||||
// Best-effort: errors are logged, not returned, so a single broken subchart
|
||||
// doesn't abort the entire release.
|
||||
func (st *HelmState) preBuildTransitiveSubchartDeps(chartPath string, helm helmexec.Interface) {
|
||||
st.prebuildChartDeps(chartPath, make(map[string]bool), helm)
|
||||
}
|
||||
|
||||
// prebuildChartDeps is the recursive worker. It visits each file:// dependency
|
||||
// depth-first, then runs helm dependency build on chartDir itself.
|
||||
func (st *HelmState) prebuildChartDeps(chartDir string, visited map[string]bool, helm helmexec.Interface) {
|
||||
absChartDir, err := filepath.Abs(chartDir)
|
||||
if err != nil || visited[absChartDir] {
|
||||
return
|
||||
}
|
||||
visited[absChartDir] = true
|
||||
|
||||
deps, ok := st.parseLocalFileDeps(absChartDir)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, depDir := range deps {
|
||||
st.prebuildChartDeps(depDir, visited, helm)
|
||||
}
|
||||
st.runHelmDepBuild(absChartDir, helm)
|
||||
}
|
||||
|
||||
// parseLocalFileDeps reads chartDir/Chart.yaml and returns the absolute paths
|
||||
// of all local file:// dependencies that exist as directories. Returns ok=false
|
||||
// only when Chart.yaml is missing, unreadable, or unparseable. When ok=true the
|
||||
// returned slice may be empty (e.g. no file:// deps present).
|
||||
func (st *HelmState) parseLocalFileDeps(chartDir string) (depDirs []string, ok bool) {
|
||||
data, err := st.fs.ReadFile(filepath.Join(chartDir, "Chart.yaml"))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type chartDep struct {
|
||||
Repository string `yaml:"repository"`
|
||||
}
|
||||
type chartMeta struct {
|
||||
Dependencies []chartDep `yaml:"dependencies,omitempty"`
|
||||
}
|
||||
var meta chartMeta
|
||||
if err := yaml.Unmarshal(data, &meta); err != nil {
|
||||
st.logger.Debugf("parseLocalFileDeps: failed to parse %s/Chart.yaml: %v", chartDir, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, dep := range meta.Dependencies {
|
||||
dir, found := st.resolveFileDep(dep.Repository, chartDir)
|
||||
if found {
|
||||
depDirs = append(depDirs, dir)
|
||||
}
|
||||
}
|
||||
return depDirs, true
|
||||
}
|
||||
|
||||
// resolveFileDep resolves a file:// repository URL to an absolute directory path.
|
||||
// Returns (dir, true) if the dependency is a local file:// URL pointing to an
|
||||
// existing directory; ("", false) otherwise.
|
||||
func (st *HelmState) resolveFileDep(repo, parentDir string) (string, bool) {
|
||||
const fileScheme = "file://"
|
||||
if !strings.HasPrefix(repo, fileScheme) {
|
||||
return "", false
|
||||
}
|
||||
path := strings.TrimPrefix(repo, fileScheme)
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(parentDir, path)
|
||||
}
|
||||
if !st.fs.DirectoryExistsAt(path) {
|
||||
return "", false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
// runHelmDepBuild runs `helm dependency build` on chartDir with a per-dir lock
|
||||
// to prevent concurrent releases from corrupting charts/*.tgz (same pattern as
|
||||
// forcedDownloadChart for #768). It dispatches through the shared helmexec
|
||||
// runner so the command inherits Helmfile's standard wiring (binary resolution,
|
||||
// flag handling, context cancellation) instead of shelling out directly. Falls
|
||||
// back to `helm dependency update` when Chart.lock is missing or stale.
|
||||
//
|
||||
// The prebuild is best-effort and runs for every transitive subchart, so it must
|
||||
// stay user-silent: helmexec.BuildDeps emits "Building dependency release=..."
|
||||
// at Info, which would clutter output for envelope charts with many subcharts.
|
||||
// We therefore run on a shallow clone whose logger is raised above Info
|
||||
// (LoggerSwapper, same pattern as helmx.go). Command wiring is shared via the
|
||||
// clone; failures are surfaced at Debug through st.logger, and the helmexec
|
||||
// ExitError message embeds helm's combined output for diagnostics.
|
||||
//
|
||||
// --skip-refresh avoids a slow `helm repo update` per subchart; file:// deps
|
||||
// (the envelope-chart use case) don't need the repo cache. For mixed file:// +
|
||||
// https:// deps, chartify's own helm dep build (without --skip-refresh) runs
|
||||
// afterwards as a safety net.
|
||||
func (st *HelmState) runHelmDepBuild(chartDir string, helm helmexec.Interface) {
|
||||
mu := st.getNamedRWMutex("prebuild-deps:" + chartDir)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Clone the runner with a quiet logger so the per-subchart
|
||||
// "Building dependency release=..." Info line and helm stdout do not leak
|
||||
// into user output. Live output is also disabled on the clone so helm stdout
|
||||
// can't bypass the logger in --enable-live-output mode. The clone shares the
|
||||
// runner and its options are a value copy, so this does not mutate the
|
||||
// original execer. Mocks that don't implement LoggerSwapper fall back to the
|
||||
// original interface (they don't emit that line anyway).
|
||||
quietHelm := helm
|
||||
if swapper, ok := helm.(helmexec.LoggerSwapper); ok {
|
||||
quietHelm = swapper.WithLogger(helmexec.NewLogger(io.Discard, "warn"))
|
||||
quietHelm.SetEnableLiveOutput(false)
|
||||
}
|
||||
|
||||
// Try `dependency build` first (honors Chart.lock). Fall back to `update`
|
||||
// (re-resolves from Chart.yaml) when the lock is missing or stale. BuildDeps
|
||||
// returns a helmexec.ExitError whose message embeds the combined output, so
|
||||
// isLockOutOfSync can inspect err.Error() without a separate os/exec call.
|
||||
if err := quietHelm.BuildDeps(filepath.Base(chartDir), chartDir, "--skip-refresh"); err != nil {
|
||||
if isLockOutOfSync(err.Error()) {
|
||||
st.logger.Debugf("runHelmDepBuild: falling back to `dependency update` for %s", chartDir)
|
||||
if err := quietHelm.UpdateDeps(chartDir); err != nil {
|
||||
st.logger.Debugf("runHelmDepBuild: helm dependency update failed for %s: %v", chartDir, err)
|
||||
}
|
||||
} else {
|
||||
st.logger.Debugf("runHelmDepBuild: helm dependency build failed for %s: %v", chartDir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isLockOutOfSync returns true when helm's error/output indicates Chart.lock is
|
||||
// missing or stale — the signal to fall back from `build` to `update`.
|
||||
func isLockOutOfSync(msg string) bool {
|
||||
return strings.Contains(msg, "out of sync") ||
|
||||
strings.Contains(msg, "lock file is out of date") ||
|
||||
strings.Contains(msg, "no lock file") ||
|
||||
strings.Contains(msg, "Chart.lock not found")
|
||||
}
|
||||
|
||||
// Otherwise, if a chart is not a helm chart, it will call "chartify" to turn it into a chart.
|
||||
//
|
||||
// If exists, it will also patch resources by json patches, strategic-merge patches, and injectors.
|
||||
// processChartification handles the chartification process
|
||||
func (st *HelmState) processChartification(chartification *Chartify, release *ReleaseSpec, chartPath string, opts ChartPrepareOptions, skipDeps bool, helmfileCommand string) (string, bool, error) {
|
||||
func (st *HelmState) processChartification(chartification *Chartify, release *ReleaseSpec, chartPath string, helm helmexec.Interface, opts ChartPrepareOptions, skipDeps bool, helmfileCommand string) (string, bool, error) {
|
||||
// Pre-build transitive local file:// subchart dependencies before chartify runs.
|
||||
// helm's `dependency build` on the parent chart fetches each file:// subchart and
|
||||
// packages it as .tgz, but does NOT recursively build the subchart's own deps.
|
||||
// Without this pre-build step, envelope charts whose subcharts have their own
|
||||
// local file:// dependencies silently drop the nested subcharts' resources, and
|
||||
// strategicMergePatches/jsonPatches targeting those resources fail with
|
||||
// "no resource matches". See https://github.com/helmfile/helmfile/issues/851.
|
||||
if !skipDeps && st.fs.DirectoryExistsAt(chartPath) {
|
||||
st.preBuildTransitiveSubchartDeps(chartPath, helm)
|
||||
}
|
||||
|
||||
// Rewrite relative file:// dependencies in Chart.yaml to absolute paths before chartify processes them
|
||||
// This prevents errors like "Error: directory /tmp/chartify.../argocd-application not found"
|
||||
// when Chart.yaml contains dependencies like "file://../argocd-application"
|
||||
@@ -2112,7 +2269,7 @@ func (st *HelmState) prepareChartForRelease(release *ReleaseSpec, helm helmexec.
|
||||
if isLocal {
|
||||
chartPath = normalizeChart(st.basePath, chartPath)
|
||||
}
|
||||
chartPath, buildDeps, err = st.processChartification(chartification, release, chartPath, opts, skipDeps, helmfileCommand)
|
||||
chartPath, buildDeps, err = st.processChartification(chartification, release, chartPath, helm, opts, skipDeps, helmfileCommand)
|
||||
if err != nil {
|
||||
return &chartPrepareResult{err: err}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
|
||||
. ${dir}/test-cases/issue-2247.sh
|
||||
. ${dir}/test-cases/issue-2097.sh
|
||||
. ${dir}/test-cases/issue-2291.sh
|
||||
. ${dir}/test-cases/issue-851.sh
|
||||
. ${dir}/test-cases/oci-parallel-pull.sh
|
||||
. ${dir}/test-cases/issue-2297-local-chart-transformers.sh
|
||||
. ${dir}/test-cases/issue-2309-kube-context-template.sh
|
||||
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Test for issue #851: Strategic merge with envelope charts does not work
|
||||
# Issue: https://github.com/helmfile/helmfile/issues/851
|
||||
#
|
||||
# Problem: When an envelope chart's subchart declares its own local file://
|
||||
# dependency, helm's `dependency build` on the parent chart fetches the subchart
|
||||
# and packages it as .tgz, but does NOT recursively build the subchart's own
|
||||
# deps first. The resulting .tgz lacks the nested subchart's resources. chartify's
|
||||
# helm template silently drops those resources, and strategicMergePatches/jsonPatches
|
||||
# targeting them fail with "no resource matches".
|
||||
#
|
||||
# Fix: preBuildTransitiveSubchartDeps walks the transitive file:// dep tree and
|
||||
# runs `helm dependency build` bottom-up on each subchart source dir before
|
||||
# chartify runs, so every subchart .tgz includes its own nested deps.
|
||||
|
||||
issue_851_input_dir="${cases_dir}/issue-851/input"
|
||||
|
||||
# Copy the chart tree to a temp dir so the test doesn't leave generated
|
||||
# Chart.lock / charts/*.tgz files in the repo. preBuildTransitiveSubchartDeps
|
||||
# runs `helm dependency build` on subchart source directories, which creates
|
||||
# those artifacts. The test must not pollute the source tree.
|
||||
issue_851_tmp_dir=$(mktemp -d)
|
||||
cp -r "${issue_851_input_dir}/." "${issue_851_tmp_dir}/"
|
||||
|
||||
cleanup_issue_851() {
|
||||
rm -rf "${issue_851_tmp_dir}"
|
||||
}
|
||||
trap cleanup_issue_851 EXIT
|
||||
|
||||
test_start "issue-851: strategic merge patches work with envelope charts"
|
||||
|
||||
info "Step 1: Templating envelope chart whose subchart has its own file:// dep"
|
||||
${helmfile} -f "${issue_851_tmp_dir}/helmfile.yaml" template > "${issue_851_tmp_dir}/templated.yaml" 2>&1
|
||||
code=$?
|
||||
|
||||
if [ $code -ne 0 ]; then
|
||||
cat "${issue_851_tmp_dir}/templated.yaml"
|
||||
fail "helmfile template failed — envelope chart with nested subchart patches should succeed"
|
||||
fi
|
||||
|
||||
info "✓ helmfile template succeeded"
|
||||
|
||||
# All three ServiceMonitors must be present, including the one defined in sub2's
|
||||
# OWN dependency (nested). Without the fix, nested-sm is silently dropped.
|
||||
for expected_name in sub1-sm sub2-sm nested-sm; do
|
||||
if ! grep -q "name: ${expected_name}" "${issue_851_tmp_dir}/templated.yaml"; then
|
||||
cat "${issue_851_tmp_dir}/templated.yaml"
|
||||
fail "Expected ServiceMonitor ${expected_name} not found (envelope chart nested subchart resources dropped)"
|
||||
fi
|
||||
info "✓ ServiceMonitor ${expected_name} present"
|
||||
done
|
||||
|
||||
# Each ServiceMonitor must reflect its strategicMergePatch.
|
||||
for expected_port in metrics-sub1 metrics-sub2 metrics-nested; do
|
||||
if ! grep -q "port: ${expected_port}" "${issue_851_tmp_dir}/templated.yaml"; then
|
||||
cat "${issue_851_tmp_dir}/templated.yaml"
|
||||
fail "Patch targeting ${expected_port} was not applied (issue 851 regression)"
|
||||
fi
|
||||
done
|
||||
info "✓ all strategicMergePatches applied (sub1-sm, sub2-sm, nested-sm)"
|
||||
|
||||
# The critical assertion: nested-sm's patch applied. Without the fix, the nested
|
||||
# subchart's resources are missing and this patch fails with "no resource matches".
|
||||
if ! grep -q "port: metrics-nested" "${issue_851_tmp_dir}/templated.yaml"; then
|
||||
cat "${issue_851_tmp_dir}/templated.yaml"
|
||||
fail "Patch for nested-sm was not applied — envelope chart regression (issue 851)"
|
||||
fi
|
||||
info "✓ nested-sm patch applied (issue 851 fixed)"
|
||||
|
||||
cleanup_issue_851
|
||||
trap - EXIT
|
||||
|
||||
test_pass "issue-851: strategic merge patches work with envelope charts"
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: envelope
|
||||
version: 0.1.0
|
||||
description: Envelope chart whose subchart has its own local file:// dep (issue 851)
|
||||
dependencies:
|
||||
- name: sub1
|
||||
version: 0.1.0
|
||||
repository: file://../sub1
|
||||
- name: sub2
|
||||
version: 0.1.0
|
||||
repository: file://../sub2
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: parent-cm
|
||||
data:
|
||||
key: parent-value
|
||||
@@ -0,0 +1,46 @@
|
||||
# Test for issue #851: Strategic merge with envelope charts does not work
|
||||
# https://github.com/helmfile/helmfile/issues/851
|
||||
#
|
||||
# Problem: When an envelope chart's subchart declares its own local file://
|
||||
# dependency, helm's `dependency build` on the parent chart fetches the subchart
|
||||
# and packages it as .tgz, but does NOT recursively build the subchart's own deps
|
||||
# first. The resulting .tgz lacks the nested subchart's resources. When chartify
|
||||
# runs helm template, resources from the nested subchart are silently absent,
|
||||
# and strategicMergePatches targeting them fail with "no resource matches".
|
||||
#
|
||||
# Fix: preBuildTransitiveSubchartDeps walks the transitive file:// dep tree and
|
||||
# runs `helm dependency build` bottom-up on each subchart source dir before
|
||||
# chartify runs, so every subchart .tgz includes its own nested deps.
|
||||
|
||||
releases:
|
||||
- name: envelope
|
||||
namespace: test
|
||||
chart: ./envelope
|
||||
strategicMergePatches:
|
||||
- apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: sub1-sm
|
||||
spec:
|
||||
endpoints:
|
||||
- port: metrics-sub1
|
||||
interval: 10s
|
||||
- apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: sub2-sm
|
||||
spec:
|
||||
endpoints:
|
||||
- port: metrics-sub2
|
||||
interval: 15s
|
||||
# The critical patch: targets a resource defined in sub2's OWN dependency
|
||||
# (nested). Without the fix, this resource is dropped from chartify's
|
||||
# patching set and the patch fails with "no resource matches".
|
||||
- apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: nested-sm
|
||||
spec:
|
||||
endpoints:
|
||||
- port: metrics-nested
|
||||
interval: 20s
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v2
|
||||
name: nested
|
||||
version: 0.1.0
|
||||
description: Deepest nested subchart — its resources are silently dropped without the fix
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: nested-sm
|
||||
spec:
|
||||
endpoints:
|
||||
- port: web
|
||||
interval: 30s
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v2
|
||||
name: sub1
|
||||
version: 0.1.0
|
||||
description: Leaf subchart (no deps)
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: sub1-sm
|
||||
spec:
|
||||
endpoints:
|
||||
- port: web
|
||||
interval: 30s
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: v2
|
||||
name: sub2
|
||||
version: 0.1.0
|
||||
description: Subchart with its own local file:// dep (the envelope chart scenario)
|
||||
dependencies:
|
||||
- name: nested
|
||||
version: 0.1.0
|
||||
repository: file://../nested
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: sub2-sm
|
||||
spec:
|
||||
endpoints:
|
||||
- port: web
|
||||
interval: 30s
|
||||
Reference in New Issue
Block a user