mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 06:44:35 +02:00
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:
co-authored by
yxxhero
parent
afdb2487a6
commit
cc85562625
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
@@ -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" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user