From 5ec1897ec725f285ea2e9c695374e462e1c67690 Mon Sep 17 00:00:00 2001 From: Aravindhan Ayyanathan Date: Thu, 20 Aug 2026 15:47:38 +0100 Subject: [PATCH 1/2] fix: Improve CEL conditional execution consistency and test coverage Signed-off-by: Aravindhan Ayyanathan --- .../en/book/04-using-functions/_index.md | 2 +- .../.expected/config.yaml | 19 ++++++++++++++ .../fn-eval/condition-invalid-cel/.krmignore | 1 + .../condition-invalid-cel/resources.yaml | 19 ++++++++++++++ .../condition-met/.expected/config.yaml | 17 ++++++++++++ e2e/testdata/fn-eval/condition-met/.krmignore | 1 + .../fn-eval/condition-met/resources.yaml | 26 +++++++++++++++++++ .../condition-not-met/.expected/config.yaml | 18 +++++++++++++ .../fn-eval/condition-not-met/.krmignore | 1 + .../fn-eval/condition-not-met/resources.yaml | 19 ++++++++++++++ .../condition-mixed/.expected/config.yaml | 7 +++++ .../condition-mixed/.expected/diff.patch | 22 ++++++++++++++++ .../condition/condition-mixed/.krmignore | 1 + .../condition/condition-mixed/Kptfile | 10 +++++++ .../condition/condition-mixed/resources.yaml | 6 +++++ .../.expected/diff.patch | 17 ++++++++++++ .../validator-excludes-own-fnconfig/Kptfile | 8 ++++++ .../deployment.yaml | 6 +++++ .../starlark-val.yaml | 10 +++++++ pkg/fn/runtime/condition_test.go | 12 ++++----- pkg/lib/runneroptions/celenv.go | 20 ++++++-------- pkg/live/load.go | 4 +-- pkg/live/rgstream.go | 6 ++--- pkg/test/runner/config.go | 2 ++ pkg/test/runner/runner.go | 3 +++ .../cmdconfig/commands/cmdeval/cmdeval.go | 2 +- 26 files changed, 234 insertions(+), 25 deletions(-) create mode 100644 e2e/testdata/fn-eval/condition-invalid-cel/.expected/config.yaml create mode 100644 e2e/testdata/fn-eval/condition-invalid-cel/.krmignore create mode 100644 e2e/testdata/fn-eval/condition-invalid-cel/resources.yaml create mode 100644 e2e/testdata/fn-eval/condition-met/.expected/config.yaml create mode 100644 e2e/testdata/fn-eval/condition-met/.krmignore create mode 100644 e2e/testdata/fn-eval/condition-met/resources.yaml create mode 100644 e2e/testdata/fn-eval/condition-not-met/.expected/config.yaml create mode 100644 e2e/testdata/fn-eval/condition-not-met/.krmignore create mode 100644 e2e/testdata/fn-eval/condition-not-met/resources.yaml create mode 100644 e2e/testdata/fn-render/condition/condition-mixed/.expected/config.yaml create mode 100644 e2e/testdata/fn-render/condition/condition-mixed/.expected/diff.patch create mode 100644 e2e/testdata/fn-render/condition/condition-mixed/.krmignore create mode 100644 e2e/testdata/fn-render/condition/condition-mixed/Kptfile create mode 100644 e2e/testdata/fn-render/condition/condition-mixed/resources.yaml create mode 100644 e2e/testdata/fn-render/validator-excludes-own-fnconfig/.expected/diff.patch create mode 100644 e2e/testdata/fn-render/validator-excludes-own-fnconfig/Kptfile create mode 100644 e2e/testdata/fn-render/validator-excludes-own-fnconfig/deployment.yaml create mode 100644 e2e/testdata/fn-render/validator-excludes-own-fnconfig/starlark-val.yaml diff --git a/documentation/content/en/book/04-using-functions/_index.md b/documentation/content/en/book/04-using-functions/_index.md index 91bded2916..2caa81174f 100644 --- a/documentation/content/en/book/04-using-functions/_index.md +++ b/documentation/content/en/book/04-using-functions/_index.md @@ -434,7 +434,7 @@ If the condition is not met: $ kpt fn render wordpress Package "wordpress": -[SKIPPED] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" (condition not met) +[SKIPPED] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" (celCondition not met) Successfully executed 0 function(s) in 1 package(s). ``` diff --git a/e2e/testdata/fn-eval/condition-invalid-cel/.expected/config.yaml b/e2e/testdata/fn-eval/condition-invalid-cel/.expected/config.yaml new file mode 100644 index 0000000000..2764cca6e3 --- /dev/null +++ b/e2e/testdata/fn-eval/condition-invalid-cel/.expected/config.yaml @@ -0,0 +1,19 @@ +# Copyright 2026 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +testType: eval +image: ghcr.io/kptdev/krm-functions-catalog/no-op +when: "this is not valid CEL" +exitCode: 1 +stdErr: "failed to compile CEL expression" diff --git a/e2e/testdata/fn-eval/condition-invalid-cel/.krmignore b/e2e/testdata/fn-eval/condition-invalid-cel/.krmignore new file mode 100644 index 0000000000..9d7a4007d6 --- /dev/null +++ b/e2e/testdata/fn-eval/condition-invalid-cel/.krmignore @@ -0,0 +1 @@ +.expected diff --git a/e2e/testdata/fn-eval/condition-invalid-cel/resources.yaml b/e2e/testdata/fn-eval/condition-invalid-cel/resources.yaml new file mode 100644 index 0000000000..28807636fa --- /dev/null +++ b/e2e/testdata/fn-eval/condition-invalid-cel/resources.yaml @@ -0,0 +1,19 @@ +# Copyright 2026 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 diff --git a/e2e/testdata/fn-eval/condition-met/.expected/config.yaml b/e2e/testdata/fn-eval/condition-met/.expected/config.yaml new file mode 100644 index 0000000000..e026ce08f5 --- /dev/null +++ b/e2e/testdata/fn-eval/condition-met/.expected/config.yaml @@ -0,0 +1,17 @@ +# Copyright 2026 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +testType: eval +image: ghcr.io/kptdev/krm-functions-catalog/no-op +when: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')" diff --git a/e2e/testdata/fn-eval/condition-met/.krmignore b/e2e/testdata/fn-eval/condition-met/.krmignore new file mode 100644 index 0000000000..9d7a4007d6 --- /dev/null +++ b/e2e/testdata/fn-eval/condition-met/.krmignore @@ -0,0 +1 @@ +.expected diff --git a/e2e/testdata/fn-eval/condition-met/resources.yaml b/e2e/testdata/fn-eval/condition-met/resources.yaml new file mode 100644 index 0000000000..bbb709d0f7 --- /dev/null +++ b/e2e/testdata/fn-eval/condition-met/resources.yaml @@ -0,0 +1,26 @@ +# Copyright 2026 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + env: production +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 diff --git a/e2e/testdata/fn-eval/condition-not-met/.expected/config.yaml b/e2e/testdata/fn-eval/condition-not-met/.expected/config.yaml new file mode 100644 index 0000000000..5943443c03 --- /dev/null +++ b/e2e/testdata/fn-eval/condition-not-met/.expected/config.yaml @@ -0,0 +1,18 @@ +# Copyright 2026 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +testType: eval +image: ghcr.io/kptdev/krm-functions-catalog/no-op +when: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')" +stdErr: "[SKIPPED]" diff --git a/e2e/testdata/fn-eval/condition-not-met/.krmignore b/e2e/testdata/fn-eval/condition-not-met/.krmignore new file mode 100644 index 0000000000..9d7a4007d6 --- /dev/null +++ b/e2e/testdata/fn-eval/condition-not-met/.krmignore @@ -0,0 +1 @@ +.expected diff --git a/e2e/testdata/fn-eval/condition-not-met/resources.yaml b/e2e/testdata/fn-eval/condition-not-met/resources.yaml new file mode 100644 index 0000000000..28807636fa --- /dev/null +++ b/e2e/testdata/fn-eval/condition-not-met/resources.yaml @@ -0,0 +1,19 @@ +# Copyright 2026 The kpt Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 diff --git a/e2e/testdata/fn-render/condition/condition-mixed/.expected/config.yaml b/e2e/testdata/fn-render/condition/condition-mixed/.expected/config.yaml new file mode 100644 index 0000000000..292ec739a0 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-mixed/.expected/config.yaml @@ -0,0 +1,7 @@ +stdErr: | + Package "condition-mixed": + [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/no-op:latest" on package "condition-mixed" + [PASS] "ghcr.io/kptdev/krm-functions-catalog/no-op:latest" in 0s + [SKIPPED] "ghcr.io/kptdev/krm-functions-catalog/no-op:latest" (celCondition not met) + + Successfully executed 1 function(s) in 1 package(s). diff --git a/e2e/testdata/fn-render/condition/condition-mixed/.expected/diff.patch b/e2e/testdata/fn-render/condition/condition-mixed/.expected/diff.patch new file mode 100644 index 0000000000..8fdee0ab68 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-mixed/.expected/diff.patch @@ -0,0 +1,22 @@ +diff --git a/Kptfile b/Kptfile +index ea7d5a8..4da1d54 100644 +--- a/Kptfile ++++ b/Kptfile +@@ -8,3 +8,17 @@ pipeline: + when: resources.exists(r, r.kind == 'ConfigMap') + - image: ghcr.io/kptdev/krm-functions-catalog/no-op + when: resources.exists(r, r.kind == 'Secret') ++status: ++ conditions: ++ - type: Rendered ++ status: "True" ++ reason: RenderSuccess ++ renderStatus: ++ mutationSteps: ++ - image: ghcr.io/kptdev/krm-functions-catalog/no-op:latest ++ exitCode: 0 ++ when: resources.exists(r, r.kind == 'ConfigMap') ++ - image: ghcr.io/kptdev/krm-functions-catalog/no-op:latest ++ exitCode: 0 ++ when: resources.exists(r, r.kind == 'Secret') ++ skipped: true diff --git a/e2e/testdata/fn-render/condition/condition-mixed/.krmignore b/e2e/testdata/fn-render/condition/condition-mixed/.krmignore new file mode 100644 index 0000000000..9d7a4007d6 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-mixed/.krmignore @@ -0,0 +1 @@ +.expected diff --git a/e2e/testdata/fn-render/condition/condition-mixed/Kptfile b/e2e/testdata/fn-render/condition/condition-mixed/Kptfile new file mode 100644 index 0000000000..ea7d5a8974 --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-mixed/Kptfile @@ -0,0 +1,10 @@ +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: condition-mixed +pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/no-op + when: resources.exists(r, r.kind == 'ConfigMap') + - image: ghcr.io/kptdev/krm-functions-catalog/no-op + when: resources.exists(r, r.kind == 'Secret') diff --git a/e2e/testdata/fn-render/condition/condition-mixed/resources.yaml b/e2e/testdata/fn-render/condition/condition-mixed/resources.yaml new file mode 100644 index 0000000000..292a53a80e --- /dev/null +++ b/e2e/testdata/fn-render/condition/condition-mixed/resources.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + env: prod diff --git a/e2e/testdata/fn-render/validator-excludes-own-fnconfig/.expected/diff.patch b/e2e/testdata/fn-render/validator-excludes-own-fnconfig/.expected/diff.patch new file mode 100644 index 0000000000..feeff49443 --- /dev/null +++ b/e2e/testdata/fn-render/validator-excludes-own-fnconfig/.expected/diff.patch @@ -0,0 +1,17 @@ +diff --git a/Kptfile b/Kptfile +index 06ea70e..7b4e0f5 100644 +--- a/Kptfile ++++ b/Kptfile +@@ -6,3 +6,12 @@ pipeline: + validators: + - image: ghcr.io/kptdev/krm-functions-catalog/starlark:latest + configPath: starlark-val.yaml ++status: ++ conditions: ++ - type: Rendered ++ status: "True" ++ reason: RenderSuccess ++ renderStatus: ++ validationSteps: ++ - image: ghcr.io/kptdev/krm-functions-catalog/starlark:latest ++ exitCode: 0 diff --git a/e2e/testdata/fn-render/validator-excludes-own-fnconfig/Kptfile b/e2e/testdata/fn-render/validator-excludes-own-fnconfig/Kptfile new file mode 100644 index 0000000000..06ea70e3f3 --- /dev/null +++ b/e2e/testdata/fn-render/validator-excludes-own-fnconfig/Kptfile @@ -0,0 +1,8 @@ +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: validator-excludes-fnconfig +pipeline: + validators: + - image: ghcr.io/kptdev/krm-functions-catalog/starlark:latest + configPath: starlark-val.yaml diff --git a/e2e/testdata/fn-render/validator-excludes-own-fnconfig/deployment.yaml b/e2e/testdata/fn-render/validator-excludes-own-fnconfig/deployment.yaml new file mode 100644 index 0000000000..073986e91d --- /dev/null +++ b/e2e/testdata/fn-render/validator-excludes-own-fnconfig/deployment.yaml @@ -0,0 +1,6 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-deployment +spec: + replicas: 1 diff --git a/e2e/testdata/fn-render/validator-excludes-own-fnconfig/starlark-val.yaml b/e2e/testdata/fn-render/validator-excludes-own-fnconfig/starlark-val.yaml new file mode 100644 index 0000000000..7d6d5736f4 --- /dev/null +++ b/e2e/testdata/fn-render/validator-excludes-own-fnconfig/starlark-val.yaml @@ -0,0 +1,10 @@ +apiVersion: fn.kpt.dev/v1alpha1 +kind: StarlarkRun +metadata: + name: starlark-val +source: |- + def validate(resource_list): + for r in resource_list["items"]: + if r["metadata"]["name"] == "starlark-val" and r["kind"] == "StarlarkRun": + fail("validator should not see its own fn-config in items") + validate(ctx.resource_list) diff --git a/pkg/fn/runtime/condition_test.go b/pkg/fn/runtime/condition_test.go index 8232ef448d..7fd9d90a2b 100644 --- a/pkg/fn/runtime/condition_test.go +++ b/pkg/fn/runtime/condition_test.go @@ -43,7 +43,7 @@ func TestFunctionRunner_Conditions(t *testing.T) { testCases := []struct { name string fn *kptfile.Function - condition string + celCond string expectRun bool }{ { @@ -51,7 +51,7 @@ func TestFunctionRunner_Conditions(t *testing.T) { fn: &kptfile.Function{ Image: runneroptions.FuncGenPkgContext, }, - condition: "resources.exists(r, r.kind == 'ConfigMap')", + celCond: "resources.exists(r, r.kind == 'ConfigMap')", expectRun: true, }, { @@ -59,7 +59,7 @@ func TestFunctionRunner_Conditions(t *testing.T) { fn: &kptfile.Function{ Image: runneroptions.FuncGenPkgContext, }, - condition: "resources.exists(r, r.kind == 'Deployment')", + celCond: "resources.exists(r, r.kind == 'Deployment')", expectRun: false, }, { @@ -67,7 +67,7 @@ func TestFunctionRunner_Conditions(t *testing.T) { fn: &kptfile.Function{ Exec: "my-exec", }, - condition: "resources.size() > 0", + celCond: "resources.size() > 0", expectRun: true, }, { @@ -75,14 +75,14 @@ func TestFunctionRunner_Conditions(t *testing.T) { fn: &kptfile.Function{ Exec: "my-exec", }, - condition: "resources.size() == 0", + celCond: "resources.size() == 0", expectRun: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - tc.fn.CelCondition = tc.condition + tc.fn.CelCondition = tc.celCond results := fnresult.NewResultList() // Mock runner options diff --git a/pkg/lib/runneroptions/celenv.go b/pkg/lib/runneroptions/celenv.go index 644a6e35f6..8c7ab955ad 100644 --- a/pkg/lib/runneroptions/celenv.go +++ b/pkg/lib/runneroptions/celenv.go @@ -31,8 +31,8 @@ type CELEnvironment struct { } // NewCELEnvironment creates a new CELEnvironment with the standard KRM variable bindings. -// Includes cel-go built-in extensions and k8s-specific validators (IP, CIDR, Quantity, SemVer) -// from k8s.io/apiserver/pkg/cel/library for full Kubernetes CEL compatibility. +// Includes cel-go built-in extensions (strings, sets, lists, comprehensions) for +// evaluating CEL expressions against KRM resources. func NewCELEnvironment() (*CELEnvironment, error) { env, err := cel.NewEnv( cel.Variable("resources", cel.ListType(cel.DynType)), @@ -54,12 +54,12 @@ func NewCELEnvironment() (*CELEnvironment, error) { // EvaluateCondition compiles and evaluates a CEL condition against a list of KRM resources. // Returns true if the condition is met, false otherwise. // An empty condition always returns true (function executes unconditionally). -func (e *CELEnvironment) EvaluateCondition(ctx context.Context, condition string, resources []*yaml.RNode, checkFrequency uint, costLimit uint64) (bool, error) { - if condition == "" { +func (e *CELEnvironment) EvaluateCondition(ctx context.Context, celCond string, resources []*yaml.RNode, checkFrequency uint, costLimit uint64) (bool, error) { + if celCond == "" { return true, nil } - ast, issues := e.env.Compile(condition) + ast, issues := e.env.Compile(celCond) if issues != nil && issues.Err() != nil { return false, fmt.Errorf("failed to compile CEL expression: %w", issues.Err()) } @@ -109,13 +109,9 @@ func resourcesToList(resources []*yaml.RNode) ([]any, error) { } func resourceToMap(resource *yaml.RNode) (map[string]any, error) { - node := resource.YNode() - if node == nil { - return nil, fmt.Errorf("resource has nil yaml.Node") - } - var result map[string]any - if err := node.Decode(&result); err != nil { - return nil, fmt.Errorf("failed to decode resource: %w", err) + result, err := resource.Map() + if err != nil { + return nil, err } ensureMetadata(result) return result, nil diff --git a/pkg/live/load.go b/pkg/live/load.go index be39fa417e..7d39bcf7ea 100644 --- a/pkg/live/load.go +++ b/pkg/live/load.go @@ -206,7 +206,7 @@ type InventoryFilter struct { } func (i *InventoryFilter) Filter(object *yaml.RNode) (*yaml.RNode, error) { - if GroupVersionKindForObject(object).String() != kptfilev1.KptFileGVK().String() { + if GroupVersionKindForObject(object) != kptfilev1.KptFileGVK() { return object, nil } @@ -241,7 +241,7 @@ func GroupVersionKindForObject(object *yaml.RNode) schema.GroupVersionKind { } func (r *RGFilter) Filter(object *yaml.RNode) (*yaml.RNode, error) { - if GroupVersionKindForObject(object).String() != rgfilev1alpha1.ResourceGroupGVK().String() { + if GroupVersionKindForObject(object) != rgfilev1alpha1.ResourceGroupGVK() { return object, nil } diff --git a/pkg/live/rgstream.go b/pkg/live/rgstream.go index 9460718120..dd5da263e0 100644 --- a/pkg/live/rgstream.go +++ b/pkg/live/rgstream.go @@ -1,4 +1,4 @@ -// Copyright 2020 The kpt Authors +// Copyright 2020,2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,8 +31,8 @@ import ( var ( excludedGKs = []schema.GroupKind{ - {Group: kptfilev1.KptFileGVK().Group, Kind: kptfilev1.KptFileGVK().Kind}, - {Group: rgfilev1alpha1.ResourceGroupGVK().Group, Kind: rgfilev1alpha1.ResourceGroupGVK().Kind}, + kptfilev1.KptFileGVK().GroupKind(), + rgfilev1alpha1.ResourceGroupGVK().GroupKind(), } ) diff --git a/pkg/test/runner/config.go b/pkg/test/runner/config.go index c5dfb6a4eb..68ee3ece0f 100644 --- a/pkg/test/runner/config.go +++ b/pkg/test/runner/config.go @@ -48,6 +48,8 @@ type EvalTestCaseConfig struct { FnConfig string `json:"fnConfig,omitempty" yaml:"fnConfig,omitempty"` // fnConfigUniquePath is an absolute, OS-specific path to function config file. fnConfigUniquePath kptfilev1.UniquePath + // When is a CEL expression that determines if the function should be run. + When string `json:"when,omitempty" yaml:"when,omitempty"` } // TestCaseConfig contains the config information for the test case diff --git a/pkg/test/runner/runner.go b/pkg/test/runner/runner.go index 2163c41a8e..a97a304425 100644 --- a/pkg/test/runner/runner.go +++ b/pkg/test/runner/runner.go @@ -216,6 +216,9 @@ func (r *Runner) runFnEval() error { if r.testCase.Config.EvalConfig.IncludeMetaResources { kptArgs = append(kptArgs, "--include-meta-resources") } + if r.testCase.Config.EvalConfig.When != "" { + kptArgs = append(kptArgs, "--when", r.testCase.Config.EvalConfig.When) + } // args must be appended last if len(r.testCase.Config.EvalConfig.Args) > 0 { kptArgs = append(kptArgs, "--") diff --git a/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go b/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go index 064c99bd49..8888368186 100644 --- a/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go +++ b/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go @@ -119,7 +119,7 @@ func GetEvalFnRunner(ctx context.Context, parent string) *EvalFnRunner { r.Command.Flags().StringArrayVar( &r.excludeLabels, "exclude-labels", []string{}, "exclude resources matching the given labels") r.Command.Flags().StringVar( - &r.CelCondition, "condition", "", "conditional expression to determine if function should be run") + &r.CelCondition, "when", "", "CEL expression to determine if the function should be run") if err := r.Command.Flags().MarkHidden("include-meta-resources"); err != nil { panic(err) From 14e03898a96e01b53fa4191a9c826c11731e8e74 Mon Sep 17 00:00:00 2001 From: Aravindhan Ayyanathan Date: Thu, 20 Aug 2026 16:00:43 +0100 Subject: [PATCH 2/2] Address review comments Signed-off-by: Aravindhan Ayyanathan --- .../en/reference/cli/fn/eval/_index.md | 7 +++ internal/docs/generated/fndocs/docs.go | 9 +++- pkg/lib/runneroptions/celenv_test.go | 54 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 pkg/lib/runneroptions/celenv_test.go diff --git a/documentation/content/en/reference/cli/fn/eval/_index.md b/documentation/content/en/reference/cli/fn/eval/_index.md index ee069e5613..3432935448 100644 --- a/documentation/content/en/reference/cli/fn/eval/_index.md +++ b/documentation/content/en/reference/cli/fn/eval/_index.md @@ -195,6 +195,13 @@ fn-args: --save, s: Save the function image and fn-config to Kptfile. Require ` + "`" + `--image` + "`" + `. +--when: + A CEL expression that determines whether the function should be run. + The expression is evaluated against the input resources and must return a boolean. + If it returns true (or is not specified), the function executes normally. + If it returns false, the function is skipped. + Example: --when "resources.exists(r, r.kind == 'ConfigMap')" + ``` #### Environment Variables diff --git a/internal/docs/generated/fndocs/docs.go b/internal/docs/generated/fndocs/docs.go index b040132753..0b2b24a16e 100644 --- a/internal/docs/generated/fndocs/docs.go +++ b/internal/docs/generated/fndocs/docs.go @@ -160,7 +160,7 @@ Flags: labels. --mount: - List of storage options to enable reading from the local filesytem. By default, + List of storage options to enable reading from the local filesystem. By default, container functions can not access the local filesystem. It accepts the same options as specified on the [Docker Volumes] for ` + "`" + `docker run` + "`" + `. All volumes are mounted readonly by default. Specify ` + "`" + `rw=true` + "`" + ` to mount volumes in read-write mode. @@ -200,6 +200,13 @@ Flags: --save, s: Save the function image and fn-config to Kptfile. Require ` + "`" + ` + "` + "`" + `" + ` + "`" + `--image` + "`" + ` + "` + "`" + `" + ` + "`" + `. + --when: + A CEL expression that determines whether the function should be run. + The expression is evaluated against the input resources and must return a boolean. + If it returns true (or is not specified), the function executes normally. + If it returns false, the function is skipped. + Example: --when "resources.exists(r, r.kind == 'ConfigMap')" + Environment Variables: diff --git a/pkg/lib/runneroptions/celenv_test.go b/pkg/lib/runneroptions/celenv_test.go new file mode 100644 index 0000000000..3ffc96bfbb --- /dev/null +++ b/pkg/lib/runneroptions/celenv_test.go @@ -0,0 +1,54 @@ +// Copyright 2026 The kpt Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runneroptions + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func TestEvaluateCondition_ResourceToMap(t *testing.T) { + env, err := NewCELEnvironment() + require.NoError(t, err) + + resource, err := yaml.Parse("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test\ndata:\n key: val\n") + require.NoError(t, err) + + // Exercises the refactored resourceToMap (RNode.Map() + ensureMetadata) path + result, err := env.EvaluateCondition(context.Background(), + `resources.exists(r, r.kind == "ConfigMap" && r.metadata.name == "test")`, + []*yaml.RNode{resource}, 100, 1000000) + require.NoError(t, err) + assert.True(t, result) +} + +func TestEvaluateCondition_EnsureMetadataDefaults(t *testing.T) { + env, err := NewCELEnvironment() + require.NoError(t, err) + + // Resource missing metadata — exercises ensureMetadata defaulting + resource, err := yaml.Parse("apiVersion: v1\nkind: ConfigMap\ndata:\n key: val\n") + require.NoError(t, err) + + result, err := env.EvaluateCondition(context.Background(), + `resources.exists(r, r.metadata.name == "")`, + []*yaml.RNode{resource}, 100, 1000000) + require.NoError(t, err) + assert.True(t, result) +}