Merge branch 'main' into fix-include-needs-transitive-1003

This commit is contained in:
copilot-swe-agent[bot]
2026-03-24 23:15:46 +00:00
33 changed files with 1127 additions and 227 deletions
+59 -31
View File
@@ -171,8 +171,9 @@ func (a *App) Diff(c DiffConfigProvider) error {
Concurrency: c.Concurrency(),
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
}, func() {
}, func() []error {
msg, matched, affected, errs = a.diff(run, c)
return errs
})
if msg != nil {
@@ -247,8 +248,9 @@ func (a *App) Template(c TemplateConfigProvider) error {
Values: c.Values(),
KubeVersion: c.KubeVersion(),
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
}, func() {
}, func() []error {
ok, errs = a.template(run, c)
return errs
})
if prepErr != nil {
@@ -269,8 +271,9 @@ func (a *App) WriteValues(c WriteValuesConfigProvider) error {
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
Concurrency: c.Concurrency(),
}, func() {
}, func() []error {
ok, errs = a.writeValues(run, c)
return errs
})
if prepErr != nil {
@@ -323,8 +326,9 @@ func (a *App) Lint(c LintConfigProvider) error {
Concurrency: c.Concurrency(),
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
}, func() {
}, func() []error {
ok, lintErrs, errs = a.lint(run, c)
return append(errs, lintErrs...)
})
if prepErr != nil {
@@ -365,8 +369,9 @@ func (a *App) Unittest(c UnittestConfigProvider) error {
Concurrency: c.Concurrency(),
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
}, func() {
}, func() []error {
ok, unittestErrs, errs = a.unittest(run, c)
return append(errs, unittestErrs...)
})
if prepErr != nil {
@@ -401,7 +406,9 @@ func (a *App) Fetch(c FetchConfigProvider) error {
OutputDir: c.OutputDir(),
OutputDirTemplate: c.OutputDirTemplate(),
Concurrency: c.Concurrency(),
}, func() {})
}, func() []error {
return nil
})
if prepErr != nil {
errs = append(errs, prepErr)
@@ -427,8 +434,9 @@ func (a *App) Sync(c SyncConfigProvider) error {
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
Validate: c.Validate(),
Concurrency: c.Concurrency(),
}, func() {
}, func() []error {
ok, errs = a.sync(run, c)
return errs
})
if prepErr != nil {
@@ -464,7 +472,7 @@ func (a *App) Apply(c ApplyConfigProvider) error {
Concurrency: c.Concurrency(),
IncludeNeeds: c.IncludeNeeds(),
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
}, func() {
}, func() []error {
matched, updated, es := a.apply(run, c)
mut.Lock()
@@ -472,6 +480,7 @@ func (a *App) Apply(c ApplyConfigProvider) error {
mut.Unlock()
ok, errs = matched, es
return errs
})
if prepErr != nil {
@@ -500,8 +509,9 @@ func (a *App) Status(c StatusesConfigProvider) error {
SkipRepos: true,
SkipDeps: true,
Concurrency: c.Concurrency(),
}, func() {
}, func() []error {
ok, errs = a.status(run, c)
return errs
})
if err != nil {
@@ -522,8 +532,9 @@ func (a *App) Destroy(c DestroyConfigProvider) error {
Concurrency: c.Concurrency(),
DeleteWait: c.DeleteWait(),
DeleteTimeout: c.DeleteTimeout(),
}, func() {
}, func() []error {
ok, errs = a.delete(run, true, c)
return errs
})
if err != nil {
errs = append(errs, err)
@@ -548,8 +559,9 @@ func (a *App) Test(c TestConfigProvider) error {
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
Concurrency: c.Concurrency(),
}, func() {
}, func() []error {
errs = a.test(run, c)
return errs
})
if err != nil {
@@ -567,11 +579,12 @@ func (a *App) PrintDAGState(c DAGConfigProvider) error {
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
}, func() {
}, func() []error {
err = a.dag(run)
if err != nil {
errs = append(errs, err)
}
return errs
})
return ok, errs
}, false, false, SetFilter(true))
@@ -583,7 +596,7 @@ func (a *App) PrintState(c StateConfigProvider) error {
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
}, func() {
}, func() []error {
if c.EmbedValues() {
for i := range run.state.Releases {
r := run.state.Releases[i]
@@ -591,7 +604,7 @@ func (a *App) PrintState(c StateConfigProvider) error {
values, err := run.state.LoadYAMLForEmbedding(&r, r.Values, r.MissingFileHandler, r.ValuesPathPrefix)
if err != nil {
errs = []error{err}
return
return errs
}
run.state.Releases[i].Values = values
@@ -599,7 +612,7 @@ func (a *App) PrintState(c StateConfigProvider) error {
secrets, err := run.state.LoadYAMLForEmbedding(&r, r.Secrets, r.MissingFileHandler, r.ValuesPathPrefix)
if err != nil {
errs = []error{err}
return
return errs
}
run.state.Releases[i].Secrets = secrets
@@ -609,17 +622,18 @@ func (a *App) PrintState(c StateConfigProvider) error {
stateYaml, err := run.state.ToYaml()
if err != nil {
errs = []error{err}
return
return errs
}
sourceFile, err := run.state.FullFilePath()
if err != nil {
errs = []error{err}
return
return errs
}
fmt.Printf("---\n# Source: %s\n\n%+v", sourceFile, stateYaml)
errs = []error{}
return errs
})
if err != nil {
@@ -648,26 +662,30 @@ func (a *App) ListReleases(c ListConfigProvider) error {
err := a.ForEachState(func(run *Run) (_ bool, errs []error) {
var stateReleases []*HelmRelease
var err error
var listErr error
if !c.SkipCharts() {
err = run.withPreparedCharts("list", state.ChartPrepareOptions{
prepErr := run.withPreparedCharts("list", state.ChartPrepareOptions{
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
}, func() {
}, func() []error {
rel, err := a.list(run)
if err != nil {
panic(err)
errs = append(errs, err)
return []error{err}
}
stateReleases = rel
return nil
})
if prepErr != nil {
errs = append(errs, prepErr)
}
} else {
stateReleases, err = a.list(run)
}
if err != nil {
errs = append(errs, err)
stateReleases, listErr = a.list(run)
if listErr != nil {
errs = append(errs, listErr)
}
}
if len(stateReleases) > 0 {
@@ -709,14 +727,16 @@ func (a *App) ListReleases(c ListConfigProvider) error {
func (a *App) list(run *Run) ([]*HelmRelease, error) {
var releases []*HelmRelease
for _, r := range run.state.Releases {
resolvedState, err := run.state.ResolveDeps()
if err != nil {
return nil, fmt.Errorf("unable to resolve dependencies for %s: %w", run.state.FilePath, err)
}
for _, r := range resolvedState.Releases {
labels := ""
if r.Labels == nil {
r.Labels = map[string]string{}
}
for k, v := range run.state.CommonLabels {
r.Labels[k] = v
}
var keys []string
for k := range r.Labels {
@@ -730,7 +750,7 @@ func (a *App) list(run *Run) ([]*HelmRelease, error) {
}
labels = strings.Trim(labels, ",")
enabled, err := state.ConditionEnabled(r, run.state.Values())
enabled, err := state.ConditionEnabled(r, resolvedState.Values())
if err != nil {
return nil, err
}
@@ -1716,6 +1736,10 @@ Do you really want to apply?
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
st.Releases = selectedAndNeededReleases
if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
return true, false, nil
}
if !interactive || interactive && r.askForConfirmation(confMsg) {
if _, preapplyErrors := withDAG(st, helm, a.Logger, state.PlanOptions{Purpose: "invoking preapply hooks for", Reverse: true, SelectedReleases: toApplyWithNeeds, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
for _, r := range subst.Releases {
@@ -1773,6 +1797,7 @@ Do you really want to apply?
Wait: c.Wait(),
WaitRetries: c.WaitRetries(),
WaitForJobs: c.WaitForJobs(),
Timeout: c.Timeout(),
ReuseValues: c.ReuseValues(),
ResetValues: c.ResetValues(),
PostRenderer: c.PostRenderer(),
@@ -1785,6 +1810,7 @@ Do you really want to apply?
TrackMode: c.TrackMode(),
TrackTimeout: c.TrackTimeout(),
TrackLogs: c.TrackLogs(),
Description: c.Description(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
}))
@@ -2241,6 +2267,7 @@ Do you really want to sync?
Wait: c.Wait(),
WaitRetries: c.WaitRetries(),
WaitForJobs: c.WaitForJobs(),
Timeout: c.Timeout(),
ReuseValues: c.ReuseValues(),
ResetValues: c.ResetValues(),
PostRenderer: c.PostRenderer(),
@@ -2253,6 +2280,7 @@ Do you really want to sync?
TrackMode: c.TrackMode(),
TrackTimeout: c.TrackTimeout(),
TrackLogs: c.TrackLogs(),
Description: c.Description(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts)
}))
+28
View File
@@ -25,6 +25,7 @@ func TestApply_2(t *testing.T) {
fields fields
ns string
concurrency int
timeout int
skipDiffOnInstall bool
error string
files map[string]string
@@ -84,6 +85,7 @@ func TestApply_2(t *testing.T) {
syncErr := app.Apply(applyConfig{
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
concurrency: tc.concurrency,
timeout: tc.timeout,
logger: logger,
skipDiffOnInstall: tc.skipDiffOnInstall,
skipNeeds: tc.fields.skipNeeds,
@@ -653,4 +655,30 @@ foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
concurrency: 1,
})
})
t.Run("timeout flag is passed to helm", func(t *testing.T) {
check(t, testcase{
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: my-release
chart: incubator/raw
namespace: default
`,
},
timeout: 300,
concurrency: 1,
upgraded: []exectest.Release{
{Name: "my-release", Flags: []string{"--timeout", "300s", "--kube-context", "default", "--namespace", "default"}},
},
diffs: map[exectest.DiffKey]error{
{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-context default --namespace default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
})
}
+158
View File
@@ -2,6 +2,7 @@ package app
import (
"bytes"
"encoding/json"
"os"
"testing"
@@ -301,3 +302,160 @@ func TestListWithJSONOutput(t *testing.T) {
testListWithJSONOutput(t, configImpl{skipCharts: true})
})
}
func TestListWithLockFileVersion(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: redis
namespace: default
chart: bitnami/redis
version: ">=1.0.0"
`,
"/path/to/helmfile.lock": `version: v0.0.0
digest: sha256:abc123
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: redis
repository: https://charts.bitnami.com/bitnami
version: 17.0.7
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
out, err := testutil.CaptureStdout(func() {
err := app.ListReleases(configImpl{skipCharts: true, output: "json"})
assert.Nil(t, err)
})
assert.NoError(t, err)
var releases []HelmRelease
if err := json.Unmarshal([]byte(out), &releases); err != nil {
t.Fatalf("failed to parse JSON output: %v", err)
}
assert.Len(t, releases, 1, "expected 1 release")
assert.Equal(t, "redis", releases[0].Name)
assert.Equal(t, "bitnami/redis", releases[0].Chart)
assert.Equal(t, "17.0.7", releases[0].Version, "expected version from helmfile.lock")
}
func TestListWithLockFileVersion_MultiFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: redis
namespace: default
chart: bitnami/redis
version: ">=1.0.0"
`,
"/path/to/helmfile.d/first.lock": `version: v0.0.0
digest: sha256:abc123
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: redis
repository: https://charts.bitnami.com/bitnami
version: 17.0.7
`,
"/path/to/helmfile.d/second.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: nginx
namespace: default
chart: bitnami/nginx
version: ">=1.0.0"
`,
"/path/to/helmfile.d/second.lock": `version: v0.0.0
digest: sha256:def456
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: nginx
repository: https://charts.bitnami.com/bitnami
version: 15.0.0
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
out, err := testutil.CaptureStdout(func() {
err := app.ListReleases(configImpl{skipCharts: true, output: "json"})
assert.Nil(t, err)
})
assert.NoError(t, err)
var releases []HelmRelease
if err := json.Unmarshal([]byte(out), &releases); err != nil {
t.Fatalf("failed to parse JSON output: %v", err)
}
assert.Len(t, releases, 2, "expected 2 releases")
releaseMap := make(map[string]HelmRelease)
for _, r := range releases {
releaseMap[r.Name] = r
}
redis := releaseMap["redis"]
assert.Equal(t, "bitnami/redis", redis.Chart)
assert.Equal(t, "17.0.7", redis.Version, "expected redis version from first.lock")
nginx := releaseMap["nginx"]
assert.Equal(t, "bitnami/nginx", nginx.Chart)
assert.Equal(t, "15.0.0", nginx.Version, "expected nginx version from second.lock")
}
+25
View File
@@ -25,6 +25,7 @@ func TestSync(t *testing.T) {
fields fields
ns string
concurrency int
timeout int
skipDiffOnInstall bool
error string
files map[string]string
@@ -81,6 +82,7 @@ func TestSync(t *testing.T) {
syncErr := app.Sync(applyConfig{
concurrency: tc.concurrency,
timeout: tc.timeout,
logger: logger,
skipDiffOnInstall: tc.skipDiffOnInstall,
skipNeeds: tc.fields.skipNeeds,
@@ -478,4 +480,27 @@ releases:
concurrency: 1,
})
})
t.Run("timeout flag is passed to helm", func(t *testing.T) {
check(t, testcase{
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: my-release
chart: incubator/raw
namespace: default
`,
},
timeout: 600,
concurrency: 1,
upgraded: []exectest.Release{
{Name: "my-release", Flags: []string{"--timeout", "600s", "--kube-context", "default", "--namespace", "default"}},
},
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
})
}
+9
View File
@@ -2388,6 +2388,7 @@ type applyConfig struct {
wait bool
waitRetries int
waitForJobs bool
timeout int
reuseValues bool
postRenderer string
postRendererArgs []string
@@ -2428,6 +2429,10 @@ func (a applyConfig) WaitForJobs() bool {
return a.waitForJobs
}
func (a applyConfig) Timeout() int {
return a.timeout
}
func (a applyConfig) Values() []string {
return a.values
}
@@ -2624,6 +2629,10 @@ func (a applyConfig) TrackLogs() bool {
return a.trackLogs
}
func (a applyConfig) Description() string {
return ""
}
type depsConfig struct {
skipRepos bool
includeNeeds bool
+121
View File
@@ -0,0 +1,121 @@
package app
import (
"sync"
"testing"
"github.com/helmfile/vals"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/exectest"
ffs "github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
)
func TestCleanupHooksErrorPropagation(t *testing.T) {
type testcase struct {
files map[string]string
releaseName string
expectedError bool
expectedInLogs 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{},
}
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
bs := runWithLogCapture(t, "info", func(t *testing.T, logger *zap.SugaredLogger) {
t.Helper()
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, tc.files)
syncErr := app.Sync(applyConfig{
concurrency: 1,
logger: logger,
})
if tc.expectedError {
assert.Error(t, syncErr, "expected error for release %s", tc.releaseName)
} else {
assert.NoError(t, syncErr, "unexpected error for release %s", tc.releaseName)
}
})
logOutput := bs.String()
assert.Contains(t, logOutput, tc.expectedInLogs, "unexpected log output")
}
t.Run("cleanup hook receives error when sync fails", func(t *testing.T) {
check(t, testcase{
releaseName: "error-release",
files: map[string]string{
"/path/to/helmfile.yaml": `
hooks:
- name: global-cleanup
events:
- cleanup
showlogs: true
command: echo
args:
- "error is '{{ .Event.Error }}'"
releases:
- name: error-release
chart: incubator/raw
namespace: default
`,
},
expectedError: true,
expectedInLogs: "error is 'failed processing release error-release: error'",
})
})
t.Run("cleanup hook receives nil when sync succeeds", func(t *testing.T) {
check(t, testcase{
releaseName: "success-release",
files: map[string]string{
"/path/to/helmfile.yaml": `
hooks:
- name: global-cleanup
events:
- cleanup
showlogs: true
command: echo
args:
- "error is '{{ .Event.Error }}'"
releases:
- name: success-release
chart: incubator/raw
namespace: default
`,
},
expectedError: false,
expectedInLogs: "error is '<nil>'",
})
})
}
+6
View File
@@ -61,6 +61,7 @@ type ApplyConfigProvider interface {
Wait() bool
WaitRetries() int
WaitForJobs() bool
Timeout() int
IncludeTests() bool
@@ -93,6 +94,8 @@ type ApplyConfigProvider interface {
TrackTimeout() int
TrackLogs() bool
Description() string
concurrencyConfig
interactive
loggingConfig
@@ -116,6 +119,7 @@ type SyncConfigProvider interface {
Wait() bool
WaitRetries() int
WaitForJobs() bool
Timeout() int
SyncArgs() string
Validate() bool
@@ -129,6 +133,8 @@ type SyncConfigProvider interface {
TrackTimeout() int
TrackLogs() bool
Description() string
DAGConfig
concurrencyConfig
+1 -1
View File
@@ -19,7 +19,7 @@ import (
const (
HelmRequiredVersion = "v3.18.6" // Minimum required version (supports Helm 3.x and 4.x)
HelmDiffRecommendedVersion = "v3.15.1"
HelmDiffRecommendedVersion = "v3.15.3"
HelmRecommendedVersion = "v4.1.0" // Recommended to use latest Helm 4
HelmSecretsRecommendedVersion = "v4.7.4" // v4.7.0+ works with both Helm 3 (single plugin) and Helm 4 (split plugin architecture)
HelmGitRecommendedVersion = "v1.3.0"
+10 -3
View File
@@ -57,7 +57,7 @@ func (r *Run) prepareChartsIfNeeded(helmfileCommand string, dir string, concurre
return releaseToChart, nil
}
func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error {
func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func() []error) error {
if r.ReleaseToChart != nil {
panic("Run.PrepareCharts can be called only once")
}
@@ -119,9 +119,16 @@ func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepare
r.ReleaseToChart = releaseToChart
f()
errs := f()
var firstErr error
for _, e := range errs {
if e != nil {
firstErr = e
break
}
}
_, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand)
_, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand, firstErr)
return err
}
@@ -2,8 +2,3 @@ merged environment: &{default map[] map[] map[]}
2 release(s) found in helmfile.yaml
Checking release existence using `helm status` for release foo_notFound
invoking preapply hooks for 1 groups of releases in this order:
GROUP RELEASES
1 default//bar, default//foo_notFound
invoking preapply hooks for releases in group 1/1: default//bar, default//foo_notFound
-7
View File
@@ -1,10 +1,3 @@
merged environment: &{default map[] map[] map[]}
2 release(s) found in helmfile.yaml
invoking preapply hooks for 2 groups of releases in this order:
GROUP RELEASES
1 default//foo
2 default//bar
invoking preapply hooks for releases in group 1/2: default//foo
invoking preapply hooks for releases in group 2/2: default//bar
@@ -0,0 +1,21 @@
merged environment: &{default map[] map[] map[]}
1 release(s) found in helmfile.yaml
Affected releases are:
my-release (incubator/raw) UPDATED
invoking preapply hooks for 1 groups of releases in this order:
GROUP RELEASES
1 default/default/my-release
invoking preapply hooks for releases in group 1/1: default/default/my-release
processing 1 groups of releases in this order:
GROUP RELEASES
1 default/default/my-release
processing releases in group 1/1: default/default/my-release
UPDATED RELEASES:
NAME NAMESPACE CHART VERSION DURATION
my-release default incubator/raw 3.1.0 0s
@@ -1,9 +1,3 @@
hook[prepare] logs | foo
hook[prepare] logs |
hook[preapply] logs | foo
hook[preapply] logs |
hook[cleanup] logs | foo
hook[cleanup] logs |
+14
View File
@@ -57,6 +57,8 @@ type ApplyOptions struct {
WaitRetries int
// WaitForJobs is true if the helm command should wait for the jobs to be completed
WaitForJobs bool
// Timeout is the timeout for helm operations in seconds
Timeout int
// Propagate '--skip-schema-validation' to helmv3 template and helm install
SkipSchemaValidation bool
// ReuseValues is true if the helm command should reuse the values
@@ -86,6 +88,8 @@ type ApplyOptions struct {
TrackTimeout int
// TrackLogs enables log streaming with kubedog
TrackLogs bool
// Description is the description that will be passed to helm upgrade --description
Description string
}
// NewApply creates a new Apply
@@ -235,6 +239,11 @@ func (a *ApplyImpl) WaitForJobs() bool {
return a.ApplyOptions.WaitForJobs
}
// Timeout returns the timeout.
func (a *ApplyImpl) Timeout() int {
return a.ApplyOptions.Timeout
}
// ReuseValues returns the ReuseValues.
func (a *ApplyImpl) ReuseValues() bool {
if !a.ResetValues() {
@@ -307,6 +316,11 @@ func (a *ApplyImpl) TrackLogs() bool {
return a.ApplyOptions.TrackLogs
}
// Description returns the description.
func (a *ApplyImpl) Description() string {
return a.ApplyOptions.Description
}
func (a *ApplyImpl) ValidateConfig() error {
validTrackModes := []string{"helm", "helm-legacy", "kubedog"}
if a.ApplyOptions.TrackMode != "" && !slices.Contains(validTrackModes, a.ApplyOptions.TrackMode) {
+7
View File
@@ -59,6 +59,8 @@ type SyncOptions struct {
TrackTimeout int
// TrackLogs enables log streaming with kubedog
TrackLogs bool
// Description is the description that will be passed to helm upgrade --description
Description string
}
// NewSyncOptions creates a new Apply
@@ -214,6 +216,11 @@ func (t *SyncImpl) TrackLogs() bool {
return t.SyncOptions.TrackLogs
}
// Description returns the description.
func (t *SyncImpl) Description() string {
return t.SyncOptions.Description
}
func (t *SyncImpl) ValidateConfig() error {
validTrackModes := []string{"helm", "helm-legacy", "kubedog"}
if t.SyncOptions.TrackMode != "" && !slices.Contains(validTrackModes, t.SyncOptions.TrackMode) {
+6
View File
@@ -28,4 +28,10 @@ const (
// Can be overridden by AWS_SDK_GO_LOG_LEVEL environment variable
// See issue #2270 and vals PR #893
AWSSDKLogLevel = "HELMFILE_AWS_SDK_LOG_LEVEL"
// ValsFailOnMissingKeyInMap controls whether vals should fail when a key is missing in a map.
// When set to "true", vals returns an error if a referenced key does not exist in the secret map.
// Default is false for backward compatibility (returns empty string for missing keys).
// See issue #1563
ValsFailOnMissingKeyInMap = "HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP"
)
+128
View File
@@ -1,8 +1,10 @@
package event
import (
"errors"
"fmt"
"io"
"strings"
"testing"
"go.uber.org/zap"
@@ -13,6 +15,11 @@ import (
)
type runner struct {
executeCalls []struct {
cmd string
args []string
env map[string]string
}
}
func (r *runner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
@@ -20,6 +27,11 @@ func (r *runner) ExecuteStdIn(cmd string, args []string, env map[string]string,
}
func (r *runner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
r.executeCalls = append(r.executeCalls, struct {
cmd string
args []string
env map[string]string
}{cmd: cmd, args: args, env: env})
if cmd == "ng" {
return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd)
}
@@ -188,3 +200,119 @@ func TestTrigger(t *testing.T) {
}
}
}
func TestTriggerCleanupEventWithError(t *testing.T) {
runner := &runner{}
core, _ := observer.New(zap.InfoLevel)
logger := zap.New(core).Sugar()
testError := errors.New("sync failed: release error")
hooks := []Hook{
{
Name: "cleanup-with-error",
Events: []string{"cleanup"},
Command: "echo",
Args: []string{"error is '{{ .Event.Error }}'"},
ShowLogs: true,
},
}
bus := &Bus{
Hooks: hooks,
StateFilePath: "/path/to/helmfile.yaml",
BasePath: ".",
Namespace: "default",
Env: environment.Environment{Name: "default"},
Logger: logger,
Fs: ffs.DefaultFileSystem(),
Runner: runner,
}
data := map[string]any{
"HelmfileCommand": "sync",
}
executed, err := bus.Trigger("cleanup", testError, data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !executed {
t.Fatal("expected cleanup hook to be executed")
}
if len(runner.executeCalls) != 1 {
t.Fatalf("expected 1 execute call, got %d", len(runner.executeCalls))
}
call := runner.executeCalls[0]
if call.cmd != "echo" {
t.Errorf("expected command 'echo', got %q", call.cmd)
}
if len(call.args) != 1 {
t.Fatalf("expected 1 arg, got %d", len(call.args))
}
expectedArg := "error is 'sync failed: release error'"
if !strings.Contains(call.args[0], "error is") {
t.Errorf("expected arg to contain 'error is', got %q", call.args[0])
}
if call.args[0] != expectedArg {
t.Errorf("expected arg %q, got %q", expectedArg, call.args[0])
}
}
func TestTriggerCleanupEventWithNilError(t *testing.T) {
runner := &runner{}
core, _ := observer.New(zap.InfoLevel)
logger := zap.New(core).Sugar()
hooks := []Hook{
{
Name: "cleanup-nil-error",
Events: []string{"cleanup"},
Command: "echo",
Args: []string{"error is '{{ .Event.Error }}'"},
ShowLogs: true,
},
}
bus := &Bus{
Hooks: hooks,
StateFilePath: "/path/to/helmfile.yaml",
BasePath: ".",
Namespace: "default",
Env: environment.Environment{Name: "default"},
Logger: logger,
Fs: ffs.DefaultFileSystem(),
Runner: runner,
}
data := map[string]any{
"HelmfileCommand": "sync",
}
executed, err := bus.Trigger("cleanup", nil, data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !executed {
t.Fatal("expected cleanup hook to be executed")
}
if len(runner.executeCalls) != 1 {
t.Fatalf("expected 1 execute call, got %d", len(runner.executeCalls))
}
call := runner.executeCalls[0]
expectedArg := "error is '<nil>'"
if call.args[0] != expectedArg {
t.Errorf("expected arg %q, got %q", expectedArg, call.args[0])
}
}
+74 -36
View File
@@ -1,8 +1,10 @@
package plugins
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
@@ -17,46 +19,82 @@ const (
)
var instance *vals.Runtime
var once sync.Once
var mu sync.Mutex
func buildValsOptions() (vals.Options, error) {
// Configure AWS SDK logging via HELMFILE_AWS_SDK_LOG_LEVEL environment variable
// Default: "off" to prevent sensitive information (tokens, auth headers) from being exposed
// See issue #2270 and vals PR helmfile/vals#893
//
// Valid values:
// - "off" (default): No AWS SDK logging - secure, prevents credential leakage
// - "minimal": Log retries only - minimal debugging info
// - "standard": Log retries + requests - moderate debugging (previous default)
// - "verbose": Log everything - full debugging (requests, responses, bodies, signing)
// - Custom: Comma-separated values like "request,response"
//
// Note: AWS_SDK_GO_LOG_LEVEL environment variable always takes precedence over this setting
// Note: Case-insensitive for known values like "off", "OFF", "Off"
logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel))
// Configure fail on missing key behavior
// Default to false for backward compatibility
// Set HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP=true to enable strict mode
// Supports common boolean values: "true", "TRUE", "1", etc.
// See issue #1563
envVal := strings.TrimSpace(os.Getenv(envvar.ValsFailOnMissingKeyInMap))
var failOnMissingKey bool
if envVal != "" {
var err error
failOnMissingKey, err = strconv.ParseBool(envVal)
if err != nil {
return vals.Options{}, fmt.Errorf("invalid value for %s: %q (must be a valid boolean)", envvar.ValsFailOnMissingKeyInMap, envVal)
}
}
// Default to "off" for security if not specified
if logLevel == "" {
logLevel = "off"
}
// Normalize known values to lowercase for case-insensitive handling
if strings.EqualFold(logLevel, "off") {
logLevel = "off"
}
opts := vals.Options{
CacheSize: valsCacheSize,
FailOnMissingKeyInMap: failOnMissingKey,
AWSLogLevel: logLevel,
}
// Also suppress vals' own internal logging unless user wants verbose output
// This prevents vals' log messages (separate from AWS SDK logs) from exposing credentials
if logLevel == "off" {
opts.LogOutput = io.Discard
}
// For other levels, allow vals to log to default output for debugging
return opts, nil
}
func ValsInstance() (*vals.Runtime, error) {
var err error
once.Do(func() {
// Configure AWS SDK logging via HELMFILE_AWS_SDK_LOG_LEVEL environment variable
// Default: "off" to prevent sensitive information (tokens, auth headers) from being exposed
// See issue #2270 and vals PR helmfile/vals#893
//
// Valid values:
// - "off" (default): No AWS SDK logging - secure, prevents credential leakage
// - "minimal": Log retries only - minimal debugging info
// - "standard": Log retries + requests - moderate debugging (previous default)
// - "verbose": Log everything - full debugging (requests, responses, bodies, signing)
// - Custom: Comma-separated values like "request,response"
//
// Note: AWS_SDK_GO_LOG_LEVEL environment variable always takes precedence over this setting
logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel))
mu.Lock()
defer mu.Unlock()
opts := vals.Options{
CacheSize: valsCacheSize,
}
if instance != nil {
return instance, nil
}
// Default to "off" for security if not specified
if logLevel == "" {
logLevel = "off"
}
opts, err := buildValsOptions()
if err != nil {
return nil, err
}
// Set AWS SDK log level for vals library
opts.AWSLogLevel = logLevel
instance, err = vals.New(opts)
if err != nil {
return nil, err
}
// Also suppress vals' own internal logging unless user wants verbose output
// This prevents vals' log messages (separate from AWS SDK logs) from exposing credentials
if logLevel == "off" {
opts.LogOutput = io.Discard
}
// For other levels, allow vals to log to default output for debugging
instance, err = vals.New(opts)
})
return instance, err
return instance, nil
}
+178 -86
View File
@@ -2,11 +2,11 @@ package plugins
import (
"io"
"os"
"strings"
"testing"
"github.com/helmfile/vals"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/envvar"
)
@@ -25,19 +25,165 @@ func TestValsInstance(t *testing.T) {
}
}
// TestAWSSDKLogLevelConfiguration tests the AWS SDK log level configuration logic
func TestBuildValsOptions(t *testing.T) {
tests := []struct {
name string
awsLogLevel string
failOnMissingKey string
expectedLogLevel string
expectedFailOnMissingKey bool
expectedLogOutputDiscarded bool
expectError bool
}{
{
name: "defaults",
awsLogLevel: "",
failOnMissingKey: "",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "explicit failOnMissingKey true",
awsLogLevel: "",
failOnMissingKey: "true",
expectedLogLevel: "off",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey false",
awsLogLevel: "",
failOnMissingKey: "false",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey with whitespace",
awsLogLevel: "",
failOnMissingKey: " true ",
expectedLogLevel: "off",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey uppercase TRUE",
awsLogLevel: "",
failOnMissingKey: "TRUE",
expectedLogLevel: "off",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey numeric 1",
awsLogLevel: "",
failOnMissingKey: "1",
expectedLogLevel: "off",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey numeric 0",
awsLogLevel: "",
failOnMissingKey: "0",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "failOnMissingKey invalid value",
awsLogLevel: "",
failOnMissingKey: "invalid",
expectError: true,
},
{
name: "aws log level verbose",
awsLogLevel: "verbose",
failOnMissingKey: "",
expectedLogLevel: "verbose",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: false,
},
{
name: "aws log level with whitespace",
awsLogLevel: " minimal ",
failOnMissingKey: "",
expectedLogLevel: "minimal",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: false,
},
{
name: "aws log level OFF uppercase",
awsLogLevel: "OFF",
failOnMissingKey: "",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "aws log level Off mixed case",
awsLogLevel: "Off",
failOnMissingKey: "",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "aws log level Off mixed case",
awsLogLevel: "Off",
failOnMissingKey: "",
expectedLogLevel: "off",
expectedFailOnMissingKey: false,
expectedLogOutputDiscarded: true,
},
{
name: "both options set",
awsLogLevel: "standard",
failOnMissingKey: "true",
expectedLogLevel: "standard",
expectedFailOnMissingKey: true,
expectedLogOutputDiscarded: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(envvar.AWSSDKLogLevel, tt.awsLogLevel)
t.Setenv(envvar.ValsFailOnMissingKeyInMap, tt.failOnMissingKey)
opts, err := buildValsOptions()
if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), envvar.ValsFailOnMissingKeyInMap)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expectedLogLevel, opts.AWSLogLevel)
assert.Equal(t, tt.expectedFailOnMissingKey, opts.FailOnMissingKeyInMap)
assert.Equal(t, valsCacheSize, opts.CacheSize)
isDiscarded := opts.LogOutput == io.Discard
assert.Equal(t, tt.expectedLogOutputDiscarded, isDiscarded)
})
}
}
func TestAWSSDKLogLevelConfiguration(t *testing.T) {
tests := []struct {
name string
envValue string
expectedLogLevel string
expectedLogOutput bool // true if LogOutput should be io.Discard
expectedLogOutput bool
}{
{
name: "no env var defaults to off",
envValue: "",
expectedLogLevel: "off",
expectedLogOutput: true, // LogOutput should be io.Discard
expectedLogOutput: true,
},
{
name: "explicit off",
@@ -45,11 +191,17 @@ func TestAWSSDKLogLevelConfiguration(t *testing.T) {
expectedLogLevel: "off",
expectedLogOutput: true,
},
{
name: "OFF uppercase",
envValue: "OFF",
expectedLogLevel: "off",
expectedLogOutput: true,
},
{
name: "minimal logging",
envValue: "minimal",
expectedLogLevel: "minimal",
expectedLogOutput: false, // LogOutput should NOT be io.Discard
expectedLogOutput: false,
},
{
name: "standard logging",
@@ -73,94 +225,34 @@ func TestAWSSDKLogLevelConfiguration(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Note: This test verifies the configuration logic, not the actual vals.New() call
// since ValsInstance() uses sync.Once and can only be initialized once per test run.
t.Setenv(envvar.AWSSDKLogLevel, tt.envValue)
// Simulate the logic from ValsInstance()
var logLevel string
if tt.envValue != "" {
logLevel = strings.TrimSpace(tt.envValue)
}
opts, err := buildValsOptions()
require.NoError(t, err)
// Default to "off" for security if not specified
if logLevel == "" {
logLevel = "off"
}
assert.Equal(t, tt.expectedLogLevel, opts.AWSLogLevel)
// Verify expected log level
if logLevel != tt.expectedLogLevel {
t.Errorf("Expected log level %q, got %q", tt.expectedLogLevel, logLevel)
}
// Verify LogOutput configuration logic
opts := vals.Options{
CacheSize: valsCacheSize,
}
opts.AWSLogLevel = logLevel
// Verify LogOutput is set to io.Discard only when level is "off"
if tt.expectedLogOutput {
opts.LogOutput = io.Discard
if opts.LogOutput != io.Discard {
t.Error("Expected LogOutput to be io.Discard for 'off' level")
}
}
isDiscarded := opts.LogOutput == io.Discard
assert.Equal(t, tt.expectedLogOutput, isDiscarded)
})
}
}
// TestEnvironmentVariableReading verifies that the HELMFILE_AWS_SDK_LOG_LEVEL env var is read correctly
func TestEnvironmentVariableReading(t *testing.T) {
tests := []struct {
name string
envValue string
expectedValue string
}{
{
name: "empty defaults to off",
envValue: "",
expectedValue: "off",
},
{
name: "whitespace trimmed",
envValue: " minimal ",
expectedValue: "minimal",
},
{
name: "standard value preserved",
envValue: "standard",
expectedValue: "standard",
},
}
func TestBuildValsOptionsIntegration(t *testing.T) {
t.Run("valid configuration produces working vals options", func(t *testing.T) {
t.Setenv(envvar.AWSSDKLogLevel, "off")
t.Setenv(envvar.ValsFailOnMissingKeyInMap, "true")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save and restore env var
original := os.Getenv(envvar.AWSSDKLogLevel)
defer func() {
if original == "" {
os.Unsetenv(envvar.AWSSDKLogLevel)
} else {
os.Setenv(envvar.AWSSDKLogLevel, original)
}
}()
opts, err := buildValsOptions()
require.NoError(t, err)
// Set test env var
if tt.envValue == "" {
os.Unsetenv(envvar.AWSSDKLogLevel)
} else {
os.Setenv(envvar.AWSSDKLogLevel, tt.envValue)
}
assert.Equal(t, valsCacheSize, opts.CacheSize)
assert.Equal(t, "off", opts.AWSLogLevel)
assert.True(t, opts.FailOnMissingKeyInMap)
assert.Equal(t, io.Discard, opts.LogOutput)
// Read and process like ValsInstance() does
logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel))
if logLevel == "" {
logLevel = "off"
}
if logLevel != tt.expectedValue {
t.Errorf("Expected %q, got %q", tt.expectedValue, logLevel)
}
})
}
rt, err := vals.New(opts)
require.NoError(t, err)
assert.NotNil(t, rt)
})
}
+16 -4
View File
@@ -140,8 +140,15 @@ func (st *HelmState) mergeLockedDependencies() (*HelmState, error) {
// When basePath is set (e.g. when loaded with baseDir instead of os.Chdir),
// resolve the lock file path relative to basePath so it can be found
// without changing the working directory.
if lockFile != "" && st.basePath != "" && !filepath.IsAbs(lockFile) {
lockFile = filepath.Join(st.basePath, lockFile)
switch {
case lockFile != "":
if st.basePath != "" && !filepath.IsAbs(lockFile) {
lockFile = filepath.Join(st.basePath, lockFile)
}
case st.basePath != "":
// When no custom lockfile is specified, use the default lockfile name
// joined with basePath to ensure it's found when not changing CWD.
lockFile = filepath.Join(st.basePath, filename+".lock")
}
depMan := NewChartDependencyManager(filename, st.logger, lockFile)
@@ -258,8 +265,13 @@ func getUnresolvedDependenciess(st *HelmState) (string, *UnresolvedDependencies)
func updateDependencies(st *HelmState, shell helmexec.DependencyUpdater, unresolved *UnresolvedDependencies, filename, wd string) (*HelmState, error) {
lockFile := st.LockFile
if lockFile != "" && st.basePath != "" && !filepath.IsAbs(lockFile) {
lockFile = filepath.Join(st.basePath, lockFile)
switch {
case lockFile != "":
if st.basePath != "" && !filepath.IsAbs(lockFile) {
lockFile = filepath.Join(st.basePath, lockFile)
}
case st.basePath != "":
lockFile = filepath.Join(st.basePath, filename+".lock")
}
depMan := NewChartDependencyManager(filename, st.logger, lockFile)
+34 -3
View File
@@ -353,7 +353,10 @@ type ReleaseSpec struct {
UnitTests []string `yaml:"unitTests,omitempty"`
// Name is the name of this release
Name string `yaml:"name,omitempty"`
Name string `yaml:"name,omitempty"`
// Description is the description for this release that will be passed to helm upgrade with --description flag
Description string `yaml:"description,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
Values []any `yaml:"values,omitempty"`
@@ -917,6 +920,7 @@ type SyncOpts struct {
TrackMode string
TrackTimeout int
TrackLogs bool
Description string
}
type SyncOpt interface{ Apply(*SyncOpts) }
@@ -3158,8 +3162,8 @@ func (st *HelmState) TriggerGlobalPrepareEvent(helmfileCommand string) (bool, er
return st.triggerGlobalReleaseEvent("prepare", nil, helmfileCommand)
}
func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string) (bool, error) {
return st.triggerGlobalReleaseEvent("cleanup", nil, helmfileCommand)
func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string, evtErr error) (bool, error) {
return st.triggerGlobalReleaseEvent("cleanup", evtErr, helmfileCommand)
}
func (st *HelmState) triggerGlobalReleaseEvent(evt string, evtErr error, helmfileCmd string) (bool, error) {
@@ -3447,6 +3451,28 @@ func (st *HelmState) appendChartDownloadFlags(flags []string, release *ReleaseSp
return flags
}
// appendDescriptionFlags appends the helm command-line flag for release description
// Command line takes precedence over config file
func (st *HelmState) appendDescriptionFlags(flags []string, release *ReleaseSpec, opt *SyncOpts, helm helmexec.Interface) ([]string, error) {
description := release.Description
if opt != nil && opt.Description != "" {
description = opt.Description
}
if description != "" {
if !helm.IsVersionAtLeast("3.3.0") {
// Determine error message based on source
if opt != nil && opt.Description != "" {
return nil, fmt.Errorf("--description flag requires Helm 3.3.0 or greater")
}
return nil, fmt.Errorf("releases[].description requires Helm 3.3.0 or greater")
}
flags = append(flags, "--description", description)
}
return flags, nil
}
func (st *HelmState) needsPlainHttp(release *ReleaseSpec, repo *RepositorySpec) bool {
var repoPlainHttp, relPlainHttp bool
if repo != nil {
@@ -3576,6 +3602,11 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = st.appendPostRenderFlags(flags, release, postRenderer, helm)
flags, err := st.appendDescriptionFlags(flags, release, opt, helm)
if err != nil {
return nil, nil, err
}
var postRendererArgs []string
if opt != nil {
postRendererArgs = opt.PostRendererArgs
+187 -5
View File
@@ -908,6 +908,188 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
"--namespace", "test-namespace",
},
},
{
name: "description-from-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--description", "Release description from config",
"--namespace", "test-namespace",
},
},
{
name: "description-from-cli",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Description: "CLI description from --description flag",
},
want: []string{
"--version", "0.1",
"--description", "CLI description from --description flag",
"--namespace", "test-namespace",
},
},
{
name: "description-cli-overrides-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{
Description: "CLI description overrides config",
},
want: []string{
"--version", "0.1",
"--description", "CLI description overrides config",
"--namespace", "test-namespace",
},
},
{
name: "description-empty-string-not-passed",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "",
},
syncOpts: &SyncOpts{
Description: "",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "description-from-config-unsupported-version-3.1.0",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.1.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
wantErr: "releases[].description requires Helm 3.3.0 or greater",
},
{
name: "description-from-config-unsupported-version-3.2.4",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.4"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
wantErr: "releases[].description requires Helm 3.3.0 or greater",
},
{
name: "description-from-cli-unsupported-version",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Description: "CLI description from --description flag",
},
wantErr: "--description flag requires Helm 3.3.0 or greater",
},
{
name: "description-empty-on-old-version",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.1.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
// No description set
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
// No --description flag should appear
},
},
{
name: "description-from-config-supported-version-3.3.0",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.3.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--description", "Release description from config",
"--namespace", "test-namespace",
},
},
}
for i := range tests {
tt := tests[i]
@@ -2548,10 +2730,10 @@ generated: 2019-05-16T15:42:45.50486+09:00
}
logger := helmexec.NewLogger(io.Discard, "debug")
basePath := "/src"
basePath := filepath.ToSlash(t.TempDir())
state := &HelmState{
basePath: basePath,
FilePath: "/src/helmfile.yaml",
FilePath: filepath.Join(basePath, "helmfile.yaml"),
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{
{
@@ -2584,8 +2766,8 @@ generated: 2019-05-16T15:42:45.50486+09:00
}
fs := testhelper.NewTestFs(map[string]string{
"/example/Chart.yaml": `foo: FOO`,
"/src/example/Chart.yaml": `foo: FOO`,
"/example/Chart.yaml": `foo: FOO`,
filepath.Join(basePath, "example/Chart.yaml"): `foo: FOO`,
})
fs.Cwd = basePath
state = injectFs(state, fs)
@@ -2648,7 +2830,7 @@ func TestHelmState_ResolveDeps_NoLockFile(t *testing.T) {
logger: logger,
fs: &filesystem.FileSystem{
ReadFile: func(f string) ([]byte, error) {
if f != "helmfile.lock" {
if f != filepath.Join("/src", "helmfile.lock") {
return nil, fmt.Errorf("stub: unexpected file: %s", f)
}
return nil, os.ErrNotExist
+6 -6
View File
@@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{
subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-5bc9c89c6b",
want: "foo-values-6ccb848dcd",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-7bf9c8bcdf",
want: "foo-values-5bcbbc4c85",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-65694d8947",
want: "foo-values-7c6468f955",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-856c5f7dd5",
want: "foo-values-8645f5847f",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-fff55fbf5",
want: "bar-values-54bd8c865",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-6bfbb74765",
want: "myns-foo-values-b4849b445",
})
for id, n := range ids {