Skip to content

feat: Add CEL-based conditional function execution (#4388)#4469

Open
SurbhiAgarwal1 wants to merge 1 commit intokptdev:mainfrom
SurbhiAgarwal1:feat/cel-clean-v2
Open

feat: Add CEL-based conditional function execution (#4388)#4469
SurbhiAgarwal1 wants to merge 1 commit intokptdev:mainfrom
SurbhiAgarwal1:feat/cel-clean-v2

Conversation

@SurbhiAgarwal1
Copy link
Copy Markdown
Contributor

@SurbhiAgarwal1 SurbhiAgarwal1 commented Apr 7, 2026

Description

This PR adds CEL-based conditional function execution to kpt, implementing #4388.

A new optional condition field is added to the Function type in the Kptfile pipeline. When specified, the CEL expression is evaluated against the current list of KRM resources. If the expression returns false, the function is skipped. If omitted or returns true, the function executes normally.

Motivation

In many real-world scenarios, you want a pipeline function to run only when certain resources exist or meet specific criteria. Without this feature, users had to maintain separate Kptfiles or manually manage which functions run. The condition field makes pipelines more dynamic and reduces the need for workarounds.

Changes

  • Added condition field to Function type in pkg/api/kptfile/v1/types.go
  • Added CELEnvironment in pkg/lib/runneroptions/celenv.go using google/cel-go
  • Integrated condition check in FunctionRunner.Filter() in internal/fnruntime/runner.go
  • Functions skipped due to condition show [SKIPPED] in CLI output
  • Added InitCELEnvironment() method to RunnerOptions for proper error handling
  • Updated all callers of InitDefaults() to also call InitCELEnvironment()
  • Added E2E testdata for condition-met and condition-not-met cases
  • Added unit tests for CEL evaluation covering all 3 runtimes (builtin, exec, container)
  • Updated documentation: kptfile schema reference and book/04-using-functions

Example Usage

pipeline:
  mutators:
    - image: ghcr.io/kptdev/krm-functions-catalog/set-namespace:v0.4
      condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'env-config')"
      configMap:
        namespace: production

Copilot AI review requested due to automatic review settings April 7, 2026 18:29
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 7, 2026

Deploy Preview for kptdocs ready!

Name Link
🔨 Latest commit 6d4aed1
🔍 Latest deploy log https://app.netlify.com/projects/kptdocs/deploys/69d6dee258b0950008381b0b
😎 Deploy Preview https://deploy-preview-4469--kptdocs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. documentation Improvements or additions to documentation enhancement New feature or request go Pull requests that update Go code labels Apr 7, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@liamfallon
Copy link
Copy Markdown
Contributor

Comment from #4391:

Closing this PR in favor of a clean rebase. The branch had accumulated 61 commits including upstream commits from other contributors, making it messy to review.

All the feedback from this review has been addressed in the new PR which has a single clean commit:

Removed unnecessary type alias file (celeval.go)
Grouped constants in const() block
Replaced interface{} with any
Removed panic from InitDefaults, added InitCELEnvironment() error method
Added e2e tests for condition-met and condition-not-met
Added immutability test
Added documentation (kptfile schema + book/04-using-functions)
Removed all unwanted files (krm-functions-catalog, output.txt, etc.)
Fixed diff.patch for renderStatus field

Thank you all for the thorough review!

Copilot AI review requested due to automatic review settings April 8, 2026 16:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

thirdparty/kyaml/runfn/runfn.go:1

  • NewFunctionRunner’s error is discarded and SetCondition is called without validating opts.CELEnvironment. This can both (a) mask construction failures and (b) silently ignore the condition at runtime when CELEnvironment is nil (since Filter() only evaluates conditions when celEnv != nil). Handle the err from NewFunctionRunner, and if r.Condition is set, return an error when opts.CELEnvironment is nil (or make SetCondition return an error and enforce it here).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return err
}
if _, err = validator.Filter(cloneResources(selectedResources)); err != nil {
validatorRunner := validator.(*fnruntime.FunctionRunner)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unchecked type assertion can panic if validator is not a *fnruntime.FunctionRunner. Prefer keeping validator typed as *fnruntime.FunctionRunner throughout, or use the “comma ok” idiom and return a descriptive error if the type is unexpected.

Suggested change
validatorRunner := validator.(*fnruntime.FunctionRunner)
validatorRunner, ok := validator.(*fnruntime.FunctionRunner)
if !ok {
err = fmt.Errorf("unexpected validator runner type %T", validator)
hctx.validationSteps = append(hctx.validationSteps, preExecFailureStep(function, err))
return err
}

Copilot uses AI. Check for mistakes.
ResolveToImage ImageResolveFunc

// CELEnvironment is the shared CEL environment used to evaluate function conditions.
// It is initialised by InitDefaults and reused across all function runners.
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says CELEnvironment is initialized by InitDefaults, but the implementation requires a separate InitCELEnvironment() call. Update the comment to match the actual initialization flow (or move initialization into InitDefaults if that’s the intended contract).

Suggested change
// It is initialised by InitDefaults and reused across all function runners.
// It is initialized separately via InitCELEnvironment and reused across all function runners.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +90
// Initialize CEL environment for condition evaluation
// Ignore error as conditions are optional; if CEL init fails, conditions will error at runtime
_ = r.RunnerOptions.InitCELEnvironment()
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring the init error undermines the “proper error handling” goal described in the PR and can degrade diagnostics (users will later see “no CEL environment configured” rather than the root init failure). Since conditions can be present in Kptfiles (not just optional CLI flags), either (1) return/init-fail early here, or (2) only ignore the error when you can prove no condition will be evaluated (which isn’t knowable for fn render). Apply the same handling pattern to other callers that currently ignore the error.

Copilot uses AI. Check for mistakes.

[SKIPPED] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" (condition not met)

Successfully executed 1 function(s) in 1 package(s).
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the “condition not met” example, the output says “Successfully executed 1 function(s)”, but the implementation/E2E expectations indicate skipped functions should not be counted as executed. Update the example output to reflect 0 executed functions when a step is skipped.

Suggested change
Successfully executed 1 function(s) in 1 package(s).
Successfully executed 0 function(s) in 1 package(s).

Copilot uses AI. Check for mistakes.
- "apply"
- "--output=json"
- "--reconcile-timeout=2m"
- "--reconcile-timeout=5m"
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description focuses on CEL-based conditional execution, but this E2E change increases reconcile timeout from 2m to 5m without being mentioned. Please either document this as part of the PR “Changes” section (with rationale), or split it into a separate PR to keep the scope aligned.

Copilot uses AI. Check for mistakes.
- "live"
- "apply"
- "--reconcile-timeout=2m"
- "--reconcile-timeout=5m"
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other live-apply fixture: this reconcile-timeout change isn’t described in the PR and appears unrelated to conditional function execution. Consider documenting the rationale in the PR or moving these timeout adjustments to a separate change.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 8, 2026 18:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 30 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

documentation/content/en/book/04-using-functions/_index.md:1

  • Two doc mismatches with the implementation: (1) the CEL resource map normalization in resourceToMap guarantees apiVersion, kind, and metadata keys, but not spec/status—either update the docs or ensure those keys are present; (2) the skipped example says “Successfully executed 1 function(s)” but the new behavior and e2e expectations indicate skipped functions should not count as executed (so this should be 0).
---

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +417 to +421
runner, _ := fnruntime.NewFunctionRunner(r.Ctx, fltr, "", fnResult, r.fnResults, opts)
if r.Condition != "" {
runner.SetCondition(r.Condition, opts.CELEnvironment)
}
return runner, nil
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewFunctionRunner returns (*FunctionRunner, error) but the error is discarded. If it ever returns a non-nil error, this will return a nil runner with a nil error (and runner.SetCondition(...) can panic). Please propagate the error, and also fail fast when r.Condition != "" but opts.CELEnvironment == nil (otherwise the condition may be silently ineffective depending on runner behavior).

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +237
func (fr *FunctionRunner) WasSkipped() bool {
return fr.skipped
}

func (fr *FunctionRunner) Filter(input []*yaml.RNode) (output []*yaml.RNode, err error) {
pr := printer.FromContextOrDie(fr.ctx)

// Check condition before executing function
if fr.celEnv != nil && fr.condition != "" {
shouldExecute, err := fr.celEnv.EvaluateCondition(fr.ctx, fr.condition, input)
if err != nil {
return nil, fmt.Errorf("failed to evaluate condition for function %q: %w", fr.name, err)
}

if !shouldExecute {
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fr.skipped (and fr.fnResult.Skipped) are only ever set to true and never reset in Filter(). If the same FunctionRunner instance is used for more than one Filter() call, WasSkipped() can remain true and fnResult.Skipped can leak across executions. Please reset fr.skipped and fr.fnResult.Skipped at the beginning of Filter() before evaluating the condition.

Copilot uses AI. Check for mistakes.
Comment on lines +242 to +249
fr.fnResult.ExitCode = 0
fr.fnResult.Skipped = true
fr.fnResults.Items = append(fr.fnResults.Items, *fr.fnResult)
// Return input unchanged - function is skipped
fr.skipped = true
return input, nil
}
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fr.skipped (and fr.fnResult.Skipped) are only ever set to true and never reset in Filter(). If the same FunctionRunner instance is used for more than one Filter() call, WasSkipped() can remain true and fnResult.Skipped can leak across executions. Please reset fr.skipped and fr.fnResult.Skipped at the beginning of Filter() before evaluating the condition.

Copilot uses AI. Check for mistakes.
Comment on lines +385 to +386
this function step (after `selectors` and `exclude` have been applied). Each resource is a map with
the standard fields: `apiVersion`, `kind`, `metadata`, `spec`, `status`.
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two doc mismatches with the implementation: (1) the CEL resource map normalization in resourceToMap guarantees apiVersion, kind, and metadata keys, but not spec/status—either update the docs or ensure those keys are present; (2) the skipped example says “Successfully executed 1 function(s)” but the new behavior and e2e expectations indicate skipped functions should not count as executed (so this should be 0).

Suggested change
this function step (after `selectors` and `exclude` have been applied). Each resource is a map with
the standard fields: `apiVersion`, `kind`, `metadata`, `spec`, `status`.
this function step (after `selectors` and `exclude` have been applied). Each resource is normalized
to include the standard fields `apiVersion`, `kind`, and `metadata`. Other fields, such as `spec`
and `status`, are available when they exist on the resource.

Copilot uses AI. Check for mistakes.
@liamfallon
Copy link
Copy Markdown
Contributor

liamfallon commented Apr 8, 2026

@SurbhiAgarwal1

The tests are failing because we have merged the conditions and renederstatus to kpt recently. I thought that by deleting the diff.patch files, the comparison would just check the output and the tests would pass. However we also need to add the difference in the Kprfile after render to the diff.patch files. Now that I deleted the diff.patch files in the PR, I can't see any option to restore them on the Github GUI.

The following `diff.patch files work for me on the tests locally on my machine:

e2e/testdata/fn-render/condition/condition-met/.expected/diff.patch

diff --git a/Kptfile b/Kptfile
index eb90ac3..d305dc0 100644
--- a/Kptfile
+++ b/Kptfile
@@ -5,4 +5,14 @@ metadata:
 pipeline:
   mutators:
     - image: ghcr.io/kptdev/krm-functions-catalog/no-op
-      condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')"
+      condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')
+status:
+  conditions:
+    - type: Rendered
+      status: "True"
+      reason: RenderSuccess
+  renderStatus:
+    mutationSteps:
+      - image: ghcr.io/kptdev/krm-functions-catalog/no-op
+        exitCode: 0

e2e/testdata/fn-render/condition/condition-not-met/.expected/diff.patch

diff --git a/Kptfile b/Kptfile
index eb90ac3..b055f32 100644
--- a/Kptfile
+++ b/Kptfile
@@ -5,4 +5,14 @@ metadata:
 pipeline:
   mutators:
     - image: ghcr.io/kptdev/krm-functions-catalog/no-op
-      condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')"
+      condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')
+status:
+  conditions:
+    - type: Rendered
+      status: "True"
+      reason: RenderSuccess
+  renderStatus:
+    mutationSteps:
+      - image: ghcr.io/kptdev/krm-functions-catalog/no-op
+        exitCode: 0
+        skipped: true

Can you try restoring the two diff.patch files with the contents above and see if that fixes the tests?

Sorry for mucking with your PR 😢

@SurbhiAgarwal1
Copy link
Copy Markdown
Contributor Author

Thank you @liamfallon! I've restored both diff.patch files with exactly the contents you provided in commit df1189f. The tests should pass now. Sorry for the confusion!

Copilot AI review requested due to automatic review settings April 8, 2026 19:04
Adds support for CEL expressions in Kptfile pipeline functions via
a new 'condition' field. Functions with a condition are only executed
if the CEL expression evaluates to true against the current resource list.

- Add CELEnvironment in pkg/lib/runneroptions/celenv.go
- Integrate condition check in FunctionRunner.Filter (runner.go)
- Append skipped result to fnResults when condition is not met
- Add 'condition' field to kptfile/v1 Function type
- Update all callers of InitDefaults to also call InitCELEnvironment
- Add e2e testdata for condition-met and condition-not-met cases
- Add unit tests for CEL evaluation and condition checking
- Update documentation: kptfile schema and book/04-using-functions
- Fix go.mod: mark k8s.io/apiserver as direct dependency

Resolves kptdev#4388

Signed-off-by: SurbhiAgarwal1 <agarwalsurbhi1807@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request go Pull requests that update Go code size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants