feat: Add ConditionTemplate support in releaseSpec (#2669)

* feat: Add ConditionTemplate support in releaseSpec

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* feat: improve testing, clarify doc

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* feat: update CHANGELOG for ConditionTemplate support and fix test cases for ID generation

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* Update pkg/state/state_exec_tmpl.go

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* Update pkg/state/state_exec_tmpl.go

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* Update docs/configuration.md

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* refactor: improve comments for condition checks and clean up whitespace

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

---------

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
Arthur Garreau
2026-07-01 18:44:43 +08:00
committed by GitHub
co-authored by yxxhero
parent afdb2487a6
commit cc85562625
10 changed files with 142 additions and 15 deletions
+6
View File
@@ -1,3 +1,9 @@
## [Unreleased]
### Added
- Add support for `conditionTemplate` and allow `condition` to be set directly to `true` or `false`.
## [1.4.1] - 2026-03-03
### Fixed
+8 -1
View File
@@ -173,7 +173,7 @@ releases:
foo: bar
chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax
version: ~1.24.1 # the semver of the chart. range constraint is supported
condition: vault.enabled # The values lookup key for filtering releases. Corresponds to the boolean value of `vault.enabled`, where `vault` is an arbitrary value
condition: vault.enabled # Filters releases by the boolean value at `vault.enabled`. Can also be set directly to true or false.
missingFileHandler: Warn # set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
missingFileHandlerConfig:
# Ignores missing git branch error so that the Debug/Info/Warn handler can treat a missing branch as non-error.
@@ -445,6 +445,12 @@ The following `helmDefaults` fields are also available but not shown in the exam
### Additional release fields
#### Condition
`condition` controls whether a release is enabled. An empty condition enables the release. A direct `true` or `false` value is treated as a literal boolean and bypasses values lookup. Any other condition must be a values lookup path ending in `.enabled`, such as `vault.enabled`.
`conditionTemplate` is evaluated before `condition` is checked. It must render to a boolean value; when both `condition` and `conditionTemplate` are set, the rendered `conditionTemplate` value replaces `condition`. Like other `*Template` fields, `conditionTemplate` is not evaluated by the `list` command.
The following per-release fields are also available:
| Field | Type | Default | Description |
@@ -457,6 +463,7 @@ The following per-release fields are also available:
| `verifyTemplate` | string | | Templated verify flag (e.g., `{{ .Values.verify \| default "false" }}`) |
| `waitTemplate` | string | | Templated wait flag |
| `installedTemplate` | string | | Templated installed flag |
| `conditionTemplate` | string | | Templated condition flag. Must render to a boolean. When set with `condition`, the rendered value replaces `condition` |
| `adopt` | list | | List of resources to adopt (passes `--adopt` to Helm) |
| `forceGoGetter` | bool | false | Force go-getter URL parsing for the chart field. Useful when go-getter URL parsing fails unexpectedly |
| `forceNamespace` | string | | Force namespace on all K8s resources rendered by the chart, even when the template doesn't use `{{ .Namespace }}`. Use with caution |
+5 -2
View File
@@ -118,12 +118,15 @@ Release Templating supports the following parts of release definition:
- basic fields: `name`, `namespace`, `chart`, `version`
- boolean fields: `installed`, `wait`, `waitForJobs`, `verify` by the means of additional text
fields designed for templating only: `installedTemplate`, `waitTemplate`, `verifyTemplate`
- boolean fields: `installed`, `wait`, `waitForJobs`, `verify`, `condition` by the means of additional text
fields designed for templating only: `installedTemplate`, `waitTemplate`, `verifyTemplate`, `conditionTemplate`.
`conditionTemplate` must render to a boolean. When set with `condition`, the rendered value replaces `condition`.
`condition` accepts direct `true`/`false` values or a values lookup path ending in `.enabled`, for example `condition: vault.enabled`.
# ...
installedTemplate: '{{`{{ eq .Release.Namespace "kube-system" }}`}}'
waitTemplate: '{{`{{ eq .Release.Labels.tag "safe" | not }}`}}'
conditionTemplate: '{{`{{ eq .Release.Namespace "kube-system" }}`}}'
# ...
- `set` block values:
+12 -3
View File
@@ -58,7 +58,7 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
ts := *result.WaitTemplate
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".waitTemplate = \"%s\": %v", r.Name, ts, err)
}
result.WaitTemplate = &resultTmpl
}
@@ -67,16 +67,25 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
ts := *result.InstalledTemplate
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".installedTemplate = \"%s\": %v", r.Name, ts, err)
}
result.InstalledTemplate = &resultTmpl
}
if result.ConditionTemplate != nil {
ts := *result.ConditionTemplate
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".conditionTemplate = \"%s\": %v", r.Name, ts, err)
}
result.ConditionTemplate = &resultTmpl
}
if result.VerifyTemplate != nil {
ts := *result.VerifyTemplate
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
if err != nil {
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".verifyTemplate = \"%s\": %v", r.Name, ts, err)
}
result.VerifyTemplate = &resultTmpl
}
+11 -1
View File
@@ -448,6 +448,7 @@ type ReleaseSpec struct {
VerifyTemplate *string `yaml:"verifyTemplate,omitempty"`
WaitTemplate *string `yaml:"waitTemplate,omitempty"`
InstalledTemplate *string `yaml:"installedTemplate,omitempty"`
ConditionTemplate *string `yaml:"conditionTemplate,omitempty"`
// These settings requires helm-x integration to work
Dependencies []Dependency `yaml:"dependencies,omitempty"`
@@ -3351,7 +3352,8 @@ func markExcludedReleases(releases []ReleaseSpec, selectors []string, values map
// ConditionEnabled checks if a release condition is enabled based on the provided values.
// It takes a ReleaseSpec and a map of values as input.
// If the condition is not specified, it returns true.
// If the condition is specified but not in the form 'foo.enabled', it returns an error.
// If the condition is a boolean literal (true/false), it returns the corresponding boolean value.
// If the condition is specified but not a boolean literal or in the form 'foo.enabled', it returns an error.
// If the condition is specified and the corresponding value is found in the values map,
// it checks if the 'enabled' field is set to true. If so, it returns true.
// Otherwise, it returns false.
@@ -3361,6 +3363,14 @@ func ConditionEnabled(r ReleaseSpec, values map[string]any) (bool, error) {
if len(r.Condition) == 0 {
return true, nil
}
switch strings.ToLower(r.Condition) {
case "true":
return true, nil
case "false":
return false, nil
}
iValues := values
keys := strings.Split(r.Condition, ".")
if keys[len(keys)-1] != "enabled" {
+9
View File
@@ -58,6 +58,15 @@ func updateBoolTemplatedValues(r *ReleaseSpec) error {
}
}
if r.ConditionTemplate != nil {
if condition, err := getBoolRefFromStringTemplate(*r.ConditionTemplate); err != nil {
return fmt.Errorf("conditionTemplate: %v", err)
} else {
r.ConditionTemplate = nil
r.Condition = fmt.Sprintf("%t", *condition)
}
}
if r.WaitTemplate != nil {
if wait, err := getBoolRefFromStringTemplate(*r.WaitTemplate); err != nil {
return fmt.Errorf("waitTemplate: %v", err)
+50 -1
View File
@@ -71,6 +71,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
Name: "app-dev",
Namespace: "dev",
Labels: map[string]string{"id": "app"},
ConditionTemplate: func(i string) *string { return &i }(`{{ eq .Release.Labels.id "app" | ternary "true" "false" }}`),
InstalledTemplate: func(i string) *string { return &i }(`{{ eq .Release.Labels.id "app" | ternary "true" "false" }}`),
VerifyTemplate: func(i string) *string { return &i }(`{{ true }}`),
Verify: func(i bool) *bool { return &i }(false),
@@ -81,11 +82,43 @@ func TestHelmState_executeTemplates(t *testing.T) {
Name: "app-dev",
Namespace: "dev",
Labels: map[string]string{"id": "app"},
Condition: "true",
Installed: func(i bool) *bool { return &i }(true),
Verify: func(i bool) *bool { return &i }(true),
Wait: func(i bool) *bool { return &i }(false),
},
},
{
name: "Condition template renders false",
input: ReleaseSpec{
Chart: "test-chart",
Name: "app-dev",
Namespace: "dev",
ConditionTemplate: func(i string) *string { return &i }(`{{ false }}`),
},
want: ReleaseSpec{
Chart: "test-chart",
Name: "app-dev",
Namespace: "dev",
Condition: "false",
},
},
{
name: "Condition template takes precedence over condition",
input: ReleaseSpec{
Chart: "test-chart",
Name: "app-dev",
Namespace: "dev",
Condition: "foo.enabled",
ConditionTemplate: func(i string) *string { return &i }(`{{ true }}`),
},
want: ReleaseSpec{
Chart: "test-chart",
Name: "app-dev",
Namespace: "dev",
Condition: "true",
},
},
{
name: "Has template in set-values",
input: ReleaseSpec{
@@ -236,6 +269,12 @@ func TestHelmState_executeTemplates(t *testing.T) {
boolPtrToString(tt.want.Wait), boolPtrToString(actual.Wait),
)
}
if !reflect.DeepEqual(actual.Condition, tt.want.Condition) {
t.Errorf("expected Condition %+v, got %+v", tt.want.Condition, actual.Condition)
}
if actual.ConditionTemplate != nil {
t.Errorf("expected ConditionTemplate to be nil, got %q", *actual.ConditionTemplate)
}
})
}
}
@@ -256,7 +295,7 @@ func TestHelmState_recursiveRefsTemplates(t *testing.T) {
},
},
{
name: "Has unresolvable boolean templates",
name: "Has unresolvable wait templates",
input: ReleaseSpec{
Name: "app-dev",
Chart: "test-charts/app",
@@ -265,6 +304,16 @@ func TestHelmState_recursiveRefsTemplates(t *testing.T) {
WaitTemplate: func(i string) *string { return &i }("hi"),
},
},
{
name: "Has unresolvable condition template",
input: ReleaseSpec{
Name: "app-dev",
Chart: "test-charts/app",
Verify: nil,
Namespace: "dev",
ConditionTemplate: func(i string) *string { return &i }(`{{ "maybe" }}`),
},
},
}
for i := range tests {
+33
View File
@@ -18,6 +18,7 @@ import (
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/testhelper"
"github.com/helmfile/helmfile/pkg/yaml"
)
var logger = helmexec.NewLogger(io.Discard, "warn")
@@ -3730,6 +3731,28 @@ func TestConditionEnabled(t *testing.T) {
condition: "",
want: true,
},
{
name: "condition true literal enabled",
condition: "true",
values: map[string]any{},
want: true,
},
{
name: "condition false literal disabled",
condition: "false",
values: map[string]any{},
want: false,
},
{
name: "condition true literal takes precedence over values lookup",
condition: "true",
values: map[string]any{
"foo": map[string]any{
"enabled": false,
},
},
want: true,
},
}
for i := range tests {
tt := tests[i]
@@ -3751,6 +3774,16 @@ func TestConditionEnabled(t *testing.T) {
}
}
func TestReleaseSpecConditionUnmarshalsBoolLiteral(t *testing.T) {
var got ReleaseSpec
require.NoError(t, yaml.Unmarshal([]byte("condition: true"), &got))
assert.Equal(t, "true", got.Condition)
enabled, err := ConditionEnabled(got, map[string]any{})
require.NoError(t, err)
assert.True(t, enabled)
}
func TestHelmState_NoReleaseMatched(t *testing.T) {
releases := []ReleaseSpec{
{
+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-6fcd6fc479",
want: "foo-values-6765d87f7c",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-6556658b7b",
want: "foo-values-6d7db5f8b",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-fd86f44b",
want: "foo-values-5c55f66dd",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-58bdc49774",
want: "foo-values-7665888bf4",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-856b998888",
want: "bar-values-bc6f974bd",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-7bdfd95fbd",
want: "myns-foo-values-76f8c7c596",
})
for id, n := range ids {
+2 -1
View File
@@ -95,7 +95,7 @@ repositories:
| `set`/`setString` | list | | Override specific values |
| `secrets` | list | | Encrypted values files (requires helm-secrets plugin) |
| `installed` | bool | | Set false to uninstall on sync |
| `condition` | string | | Values lookup key for filtering releases |
| `condition` | string | | Direct `true`/`false` or values lookup key ending in `.enabled` for filtering releases |
| `wait` | bool | false | Wait for resources to be ready |
| `waitForJobs` | bool | false | Wait until all Jobs have completed |
| `timeout` | int | 300 | Operation timeout in seconds |
@@ -112,6 +112,7 @@ repositories:
| `verifyTemplate` | string | | Templated verify flag |
| `waitTemplate` | string | | Templated wait flag |
| `installedTemplate` | string | | Templated installed flag |
| `conditionTemplate` | string | | Templated condition flag. Must render to boolean. When set with `condition`, the rendered value replaces `condition` |
| `adopt` | list | | Resources to adopt (passes `--adopt` to Helm) |
| `forceGoGetter` | bool | false | Force go-getter URL parsing for chart field |
| `forceNamespace` | string | | Force namespace on all K8s resources |