mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-09 12:37:32 +02:00
--include-needs was incorrectly including transitive dependencies when it should only include direct dependencies per the documentation. The issue was that ChartPrepareOptions.IncludeTransitiveNeeds was being set to c.IncludeNeeds() instead of c.IncludeTransitiveNeeds() in the chart preparation phase for diff, template, lint, sync, and apply commands. This fix ensures: - --include-needs includes only direct dependencies - --include-transitive-needs includes both direct and transitive dependencies Fixes #1003 Signed-off-by: yxxhero <yxxhero@users.noreply.github.com> Signed-off-by: yxxhero <aiopsclub@163.com>
531 lines
15 KiB
Go
531 lines
15 KiB
Go
package app
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/helmfile/vals"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/helmfile/helmfile/pkg/exectest"
|
|
ffs "github.com/helmfile/helmfile/pkg/filesystem"
|
|
"github.com/helmfile/helmfile/pkg/helmexec"
|
|
"github.com/helmfile/helmfile/pkg/runtime"
|
|
)
|
|
|
|
func TestTemplate(t *testing.T) {
|
|
type fields struct {
|
|
skipNeeds bool
|
|
includeNeeds bool
|
|
includeTransitiveNeeds bool
|
|
noHooks bool
|
|
showOnly []string
|
|
}
|
|
|
|
type testcase struct {
|
|
fields fields
|
|
ns string
|
|
error string
|
|
selectors []string
|
|
templated []exectest.Release
|
|
}
|
|
|
|
check := func(t *testing.T, tc testcase) {
|
|
t.Helper()
|
|
|
|
wantTemplates := tc.templated
|
|
|
|
var helm = &exectest.Helm{
|
|
FailOnUnexpectedList: true,
|
|
FailOnUnexpectedDiff: true,
|
|
DiffMutex: &sync.Mutex{},
|
|
ChartsMutex: &sync.Mutex{},
|
|
ReleasesMutex: &sync.Mutex{},
|
|
}
|
|
|
|
bs := runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
|
t.Helper()
|
|
|
|
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
|
if err != nil {
|
|
t.Errorf("unexpected error creating vals runtime: %v", err)
|
|
}
|
|
|
|
files := map[string]string{
|
|
"/path/to/helmfile.yaml": `
|
|
releases:
|
|
- name: logging
|
|
chart: incubator/raw
|
|
namespace: kube-system
|
|
|
|
- name: kubernetes-external-secrets
|
|
chart: incubator/raw
|
|
namespace: kube-system
|
|
needs:
|
|
- kube-system/logging
|
|
|
|
- name: external-secrets
|
|
chart: incubator/raw
|
|
namespace: default
|
|
labels:
|
|
app: test
|
|
needs:
|
|
- kube-system/kubernetes-external-secrets
|
|
|
|
- name: my-release
|
|
chart: incubator/raw
|
|
namespace: default
|
|
labels:
|
|
app: test
|
|
needs:
|
|
- default/external-secrets
|
|
|
|
|
|
# Disabled releases are treated as missing
|
|
- name: disabled
|
|
chart: incubator/raw
|
|
namespace: kube-system
|
|
installed: false
|
|
|
|
- name: test2
|
|
chart: incubator/raw
|
|
needs:
|
|
- kube-system/disabled
|
|
|
|
- name: test3
|
|
chart: incubator/raw
|
|
needs:
|
|
- test2
|
|
`,
|
|
}
|
|
|
|
app := appWithFs(&App{
|
|
OverrideHelmBinary: DefaultHelmBinary,
|
|
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
|
OverrideKubeContext: "default",
|
|
DisableKubeVersionAutoDetection: true,
|
|
Env: "default",
|
|
Logger: logger,
|
|
helms: map[helmKey]helmexec.Interface{
|
|
createHelmKey("helm", "default"): helm,
|
|
},
|
|
valsRuntime: valsRuntime,
|
|
}, files)
|
|
|
|
if tc.ns != "" {
|
|
app.Namespace = tc.ns
|
|
}
|
|
|
|
if tc.selectors != nil {
|
|
app.Selectors = tc.selectors
|
|
}
|
|
|
|
tmplErr := app.Template(applyConfig{
|
|
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
|
|
concurrency: 1,
|
|
logger: logger,
|
|
skipNeeds: tc.fields.skipNeeds,
|
|
includeNeeds: tc.fields.includeNeeds,
|
|
includeTransitiveNeeds: tc.fields.includeTransitiveNeeds,
|
|
showOnly: tc.fields.showOnly,
|
|
noHooks: tc.fields.noHooks,
|
|
})
|
|
|
|
var gotErr string
|
|
if tmplErr != nil {
|
|
gotErr = tmplErr.Error()
|
|
}
|
|
|
|
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
|
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
|
}
|
|
|
|
require.Equal(t, wantTemplates, helm.Templated)
|
|
})
|
|
|
|
testNameComponents := strings.Split(t.Name(), "/")
|
|
testBaseName := strings.ToLower(
|
|
strings.ReplaceAll(
|
|
testNameComponents[len(testNameComponents)-1],
|
|
" ",
|
|
"_",
|
|
),
|
|
)
|
|
wantLogFileDir := filepath.Join("testdata", "app_template_test")
|
|
wantLogFile := filepath.Join(wantLogFileDir, testBaseName)
|
|
wantLogData, err := os.ReadFile(wantLogFile)
|
|
updateLogFile := err != nil
|
|
wantLog := string(wantLogData)
|
|
gotLog := bs.String()
|
|
if updateLogFile {
|
|
if err := os.MkdirAll(wantLogFileDir, 0755); err != nil {
|
|
t.Fatalf("unable to create directory %q: %v", wantLogFileDir, err)
|
|
}
|
|
if err := os.WriteFile(wantLogFile, bs.Bytes(), 0644); err != nil {
|
|
t.Fatalf("unable to update lint log snapshot: %v", err)
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, wantLog, gotLog)
|
|
}
|
|
|
|
t.Run("fail on unselected need by default", func(t *testing.T) {
|
|
check(t, testcase{
|
|
selectors: []string{"app=test"},
|
|
error: `in ./helmfile.yaml: release "default/default/external-secrets" depends on "default/kube-system/kubernetes-external-secrets" which does not match the selectors. Please add a selector like "--selector name=kubernetes-external-secrets", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies`,
|
|
})
|
|
})
|
|
|
|
t.Run("skip-needs", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: true,
|
|
},
|
|
selectors: []string{"app=test"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "external-secrets", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
|
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("include-needs", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: false,
|
|
includeNeeds: true,
|
|
},
|
|
error: ``,
|
|
selectors: []string{"app=test"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
// Issue #1003: --include-needs should only include direct dependencies, not transitive
|
|
templated: []exectest.Release{
|
|
{Name: "kubernetes-external-secrets", Flags: []string{"--kube-context", "default", "--namespace", "kube-system"}},
|
|
{Name: "external-secrets", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
|
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("include-transitive-needs", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: false,
|
|
includeTransitiveNeeds: true,
|
|
},
|
|
error: ``,
|
|
selectors: []string{"app=test"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "logging", Flags: []string{"--kube-context", "default", "--namespace", "kube-system"}},
|
|
{Name: "kubernetes-external-secrets", Flags: []string{"--kube-context", "default", "--namespace", "kube-system"}},
|
|
{Name: "external-secrets", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
|
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("include-needs should not fail on disabled direct need", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: false,
|
|
includeNeeds: true,
|
|
},
|
|
selectors: []string{"name=test2"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "test2", Flags: []string{"--kube-context", "default"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("include-needs should not fail on disabled transitive need", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: false,
|
|
includeNeeds: true,
|
|
},
|
|
selectors: []string{"name=test3"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "test2", Flags: []string{"--kube-context", "default"}},
|
|
{Name: "test3", Flags: []string{"--kube-context", "default"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("include-transitive-needs should not fail on disabled transitive need", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: false,
|
|
includeNeeds: false,
|
|
includeTransitiveNeeds: true,
|
|
},
|
|
selectors: []string{"name=test3"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "test2", Flags: []string{"--kube-context", "default"}},
|
|
{Name: "test3", Flags: []string{"--kube-context", "default"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("include-needs with include-transitive-needs should not fail on disabled direct need", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: false,
|
|
includeNeeds: true,
|
|
includeTransitiveNeeds: true,
|
|
},
|
|
selectors: []string{"name=test2"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "test2", Flags: []string{"--kube-context", "default"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("include-needs with include-transitive-needs should not fail on disabled transitive need", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: false,
|
|
includeNeeds: true,
|
|
includeTransitiveNeeds: true,
|
|
},
|
|
selectors: []string{"name=test3"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "test2", Flags: []string{"--kube-context", "default"}},
|
|
{Name: "test3", Flags: []string{"--kube-context", "default"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("no-hooks", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
skipNeeds: true,
|
|
noHooks: true,
|
|
},
|
|
selectors: []string{"app=test"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "external-secrets", Flags: []string{"--kube-context", "default", "--namespace", "default", "--no-hooks"}},
|
|
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default", "--no-hooks"}},
|
|
},
|
|
})
|
|
})
|
|
|
|
t.Run("bad selector", func(t *testing.T) {
|
|
check(t, testcase{
|
|
selectors: []string{"app=test_non_existent"},
|
|
templated: nil,
|
|
error: "err: no releases found that matches specified selector(app=test_non_existent) and environment(default), in any helmfile",
|
|
})
|
|
})
|
|
|
|
t.Run("show-only", func(t *testing.T) {
|
|
check(t, testcase{
|
|
fields: fields{
|
|
showOnly: []string{"templates/resources.yaml"},
|
|
},
|
|
selectors: []string{"name=logging"},
|
|
// Issue #2309: --kube-context is now added to template flags
|
|
templated: []exectest.Release{
|
|
{Name: "logging", Flags: []string{"--show-only", "templates/resources.yaml", "--kube-context", "default", "--namespace", "kube-system"}},
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestTemplate_StrictParsing(t *testing.T) {
|
|
type testcase struct {
|
|
GoYamlV3 bool
|
|
ns string
|
|
error string
|
|
}
|
|
|
|
check := func(t *testing.T, tc testcase) {
|
|
t.Helper()
|
|
|
|
v := runtime.GoYamlV3
|
|
runtime.GoYamlV3 = tc.GoYamlV3
|
|
t.Cleanup(func() {
|
|
runtime.GoYamlV3 = v
|
|
})
|
|
|
|
var helm = &exectest.Helm{
|
|
FailOnUnexpectedList: true,
|
|
FailOnUnexpectedDiff: true,
|
|
DiffMutex: &sync.Mutex{},
|
|
ChartsMutex: &sync.Mutex{},
|
|
ReleasesMutex: &sync.Mutex{},
|
|
}
|
|
|
|
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
|
t.Helper()
|
|
|
|
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
|
if err != nil {
|
|
t.Errorf("unexpected error creating vals runtime: %v", err)
|
|
}
|
|
|
|
files := map[string]string{
|
|
"/path/to/helmfile.yaml": `
|
|
releases:
|
|
- name: app1
|
|
foobar: FOOBAR
|
|
chart: incubator/raw
|
|
`,
|
|
}
|
|
|
|
app := appWithFs(&App{
|
|
OverrideHelmBinary: DefaultHelmBinary,
|
|
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
|
OverrideKubeContext: "default",
|
|
DisableKubeVersionAutoDetection: true,
|
|
Env: "default",
|
|
Logger: logger,
|
|
helms: map[helmKey]helmexec.Interface{
|
|
createHelmKey("helm", "default"): helm,
|
|
},
|
|
valsRuntime: valsRuntime,
|
|
}, files)
|
|
|
|
if tc.ns != "" {
|
|
app.Namespace = tc.ns
|
|
}
|
|
|
|
tmplErr := app.Template(applyConfig{
|
|
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
|
|
concurrency: 1,
|
|
logger: logger,
|
|
})
|
|
|
|
var gotErr string
|
|
if tmplErr != nil {
|
|
gotErr = tmplErr.Error()
|
|
}
|
|
|
|
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
|
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("fail due to unknown field with go.yaml.in/yaml/v3", func(t *testing.T) {
|
|
check(t, testcase{
|
|
GoYamlV3: true,
|
|
error: `in ./helmfile.yaml: failed to read helmfile.yaml: reading document at index 1. Started seeing this since Helmfile v1? Add the .gotmpl file extension: yaml: unmarshal errors:
|
|
line 4: field foobar not found in type state.ReleaseSpec`,
|
|
})
|
|
})
|
|
|
|
t.Run("fail due to unknown field with go.yaml.in/yaml/v2", func(t *testing.T) {
|
|
check(t, testcase{
|
|
GoYamlV3: false,
|
|
error: `in ./helmfile.yaml: failed to read helmfile.yaml: reading document at index 1. Started seeing this since Helmfile v1? Add the .gotmpl file extension: yaml: unmarshal errors:
|
|
line 4: field foobar not found in type state.ReleaseSpec`,
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestTemplate_CyclicInheritance(t *testing.T) {
|
|
type testcase struct {
|
|
ns string
|
|
error string
|
|
}
|
|
|
|
check := func(t *testing.T, tc testcase) {
|
|
t.Helper()
|
|
|
|
var helm = &exectest.Helm{
|
|
FailOnUnexpectedList: true,
|
|
FailOnUnexpectedDiff: true,
|
|
DiffMutex: &sync.Mutex{},
|
|
ChartsMutex: &sync.Mutex{},
|
|
ReleasesMutex: &sync.Mutex{},
|
|
}
|
|
|
|
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
|
t.Helper()
|
|
|
|
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
|
if err != nil {
|
|
t.Errorf("unexpected error creating vals runtime: %v", err)
|
|
}
|
|
|
|
files := map[string]string{
|
|
"/path/to/helmfile.yaml": `
|
|
templates:
|
|
a:
|
|
inherit:
|
|
- template: b
|
|
values:
|
|
- a.yaml
|
|
b:
|
|
inherit:
|
|
- template: c
|
|
values:
|
|
- b.yaml
|
|
c:
|
|
inherit:
|
|
- template: a
|
|
values:
|
|
- c.yaml
|
|
releases:
|
|
- name: app1
|
|
inherit:
|
|
- template: a
|
|
chart: incubator/raw
|
|
`,
|
|
}
|
|
|
|
app := appWithFs(&App{
|
|
OverrideHelmBinary: DefaultHelmBinary,
|
|
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
|
OverrideKubeContext: "default",
|
|
DisableKubeVersionAutoDetection: true,
|
|
Env: "default",
|
|
Logger: logger,
|
|
helms: map[helmKey]helmexec.Interface{
|
|
createHelmKey("helm", "default"): helm,
|
|
},
|
|
valsRuntime: valsRuntime,
|
|
}, files)
|
|
|
|
if tc.ns != "" {
|
|
app.Namespace = tc.ns
|
|
}
|
|
|
|
tmplErr := app.Template(applyConfig{
|
|
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
|
|
concurrency: 1,
|
|
logger: logger,
|
|
})
|
|
|
|
var gotErr string
|
|
if tmplErr != nil {
|
|
gotErr = tmplErr.Error()
|
|
}
|
|
|
|
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
|
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("fail due to cyclic inheritance", func(t *testing.T) {
|
|
check(t, testcase{
|
|
error: `in ./helmfile.yaml: failed executing release templates in "helmfile.yaml": unable to load release "app1" with template: cyclic inheritance detected: a->b->c->a`,
|
|
})
|
|
})
|
|
}
|