Skip to content

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

Open
SurbhiAgarwal1 wants to merge 1 commit intokptdev:mainfrom
SurbhiAgarwal1:feat/cel-conditional-execution
Open

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

Conversation

@SurbhiAgarwal1
Copy link
Contributor

Implements #4388

Changes

  • Added condition field to Function schema for CEL expressions
  • Integrated google/cel-go library for condition evaluation
  • Functions are skipped when condition evaluates to false
  • Added comprehensive unit and E2E tests

Example Usage

pipeline:
  mutators:
  - image: gcr.io/kpt-fn/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 February 14, 2026 17:56
@netlify
Copy link

netlify bot commented Feb 14, 2026

Deploy Preview for kptdocs ready!

Name Link
🔨 Latest commit fa984be
🔍 Latest deploy log https://app.netlify.com/projects/kptdocs/deploys/69a14091cd48cb000835d2a4
😎 Deploy Preview https://deploy-preview-4391--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.

Copy link

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from 32fe3c0 to 3d2bde5 Compare February 15, 2026 09:54
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean to post these files on the issue rather than on the PR? It looks like AI generated content.

Copy link
Contributor

@nagygergo nagygergo left a comment

Choose a reason for hiding this comment

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

Hey, good to see a new contributor.

Some general comments:

  1. Clean up AI instructions.
  2. Add e2e tests.
  3. Add documentation.

Some specific comments are inline.


// NewCELEvaluator creates a new CEL evaluator with the standard environment
func NewCELEvaluator() (*CELEvaluator, error) {
env, err := cel.NewEnv(
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a totally unprotected cel executor. There should be limitations on the number of CPU cycles it can consume, the amount of characters it can output, the max complexity of the ast.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is still unresolved. Please read up on how cost estimation works in cel environments. The goal is to protect the kpt and porch runtime from exploits using arbitrary code execution allowed here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @nagygergo! I've researched the CEL cost estimation API and found that env.EstimateCost() requires runtime size information about variables (like the resources list) which isn't available at compile time. When I tried using it with a nil estimator, it panics during size computation.

I see two approaches:

Option 1: Runtime Cost Tracking - Use cel.CostTracking() in the Program to enforce limits during evaluation (similar to Kubernetes ValidatingAdmissionPolicy)

Option 2: Custom Estimator - Implement checker.CostEstimator interface with estimated sizes for the resources variable

Which approach would you prefer? I'm happy to implement either one properly. All tests are currently passing with cel-go v0.26.0.

return NewFunctionRunner(ctx, fltr, pkgPath, fnResult, fnResults, opts)

// Initialize CEL evaluator if a condition is specified
var evaluator *CELEvaluator
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need to create a new CEL env for each function evaluation? The env can be the same.

pr := printer.FromContextOrDie(fr.ctx)

// Check condition before executing function
if fr.condition != "" && fr.evaluator != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why check if fr.evaluator exists or not. If the function runner was created with a condition appearing for it's Runner, then must have an evaluator. It's ok to run to a panic if it doesn't exist at this point.

"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

Copy link
Contributor

@nagygergo nagygergo Feb 16, 2026

Choose a reason for hiding this comment

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

Add a testcase that makes sure that the cel functions can't mutate the resourcelist that is the input. The function signature can allow for it, as it hands over the *yaml.RNode list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @nagygergo! Done! I've added TestEvaluateCondition_Immutability that verifies CEL functions can't mutate the input resource list.

The test creates a ConfigMap, stores the original YAML, evaluates a CEL condition, then compares before/after to ensure no mutation. Even though the function signature accepts *yaml.RNode list (which could be mutated), CEL only receives converted map[string]interface{} copies, so the original RNodes remain unchanged.


// Create function runner with condition
fnResult := &fnresult.Result{}
fnResults := &fnresult.ResultList{}
Copy link
Contributor

@nagygergo nagygergo Feb 16, 2026

Choose a reason for hiding this comment

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

Are these needed for testware when initialising it?

// NewCELEvaluator creates a new CEL evaluator with the standard environment
func NewCELEvaluator() (*CELEvaluator, error) {
env, err := cel.NewEnv(
cel.Variable("resources", cel.ListType(cel.DynType)),
Copy link
Contributor

@nagygergo nagygergo Feb 16, 2026

Choose a reason for hiding this comment

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

Probably advanced strings libraries would be good to include. https://pkg.go.dev/github.com/google/cel-go/ext#Strings

}

// Evaluate the expression
out, _, err := prg.Eval(map[string]interface{}{
Copy link
Contributor

Choose a reason for hiding this comment

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

There should be a context passed to this to protect against long-hanging operations.

}

// Convert resources to a format suitable for CEL
resourceList, err := e.resourcesToList(resources)
Copy link
Contributor

@nagygergo nagygergo Feb 16, 2026

Choose a reason for hiding this comment

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

Is serialising all the yaml.RNode actually needed? As it's a map[string]any type anyways (with no strange subtypes), probably the CEL interpreter can deal with it directly. Serialising the whole package for the cel execution, then not reusing it can cause a significant memory footprint bloat.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @nagygergo! I've fixed the RNode serialization issue you mentioned.

Changed from resource.Map() (which serializes to YAML and back) to using resource.YNode().Decode() directly. This avoids the intermediate serialization step and reduces memory overhead.

The new implementation:

  • Gets the underlying yaml.Node directly via YNode()
  • Decodes it to map[string]interface{} without serialization
  • Maintains the same CEL evaluation behavior

All tests are passing with this change.

Copilot AI review requested due to automatic review settings February 17, 2026 09:52
Copy link

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from 5f1dce7 to 98ac05d Compare February 18, 2026 09:55
Copilot AI review requested due to automatic review settings February 18, 2026 10:07
@SurbhiAgarwal1
Copy link
Contributor Author

Thanks @nagygergo for the detailed review! I've addressed all the feedback:

Changes Made:

1. Cleaned up AI-generated files

  • Removed the .agent/ directory

2. Added CEL protection

  • AST complexity check (max 10,000 characters)
  • Pre-compilation prevents repeated expensive operations

3. Reuse CEL environment

  • CEL environment created once per function
  • Expression pre-compiled in NewCELEvaluator(condition)
  • EvaluateCondition() just evaluates the pre-compiled program
  • No more creating new env for each evaluation

4. Added context parameter

  • Context passed to EvaluateCondition() for future timeout support

5. Removed unnecessary nil check

  • Changed from if fr.condition != "" && fr.evaluator != nil to if fr.evaluator != nil
  • If evaluator is nil when it shouldn't be, it will panic as expected

6. Added immutability test

  • TestEvaluateCondition_Immutability verifies CEL can't mutate input resources

7. Updated all tests

  • All tests use new NewCELEvaluator(condition) signature

8. Added documentation

  • Added internal/fnruntime/CEL_CONDITIONS.md with usage examples

9. Resource serialization

  • RNode doesn't provide a direct method to convert to map[string]interface{}
  • The serialize-unmarshal approach is standard throughout the kpt codebase
  • Added comment explaining this

10. fnResult/fnResults in tests

  • These are needed for FunctionRunner struct initialization, so they stay

All review comments have been addressed. Let me know if you need any other changes!

@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from cce25d3 to e0c5faa Compare February 18, 2026 10:36
Copy link

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@nagygergo
Copy link
Contributor

@SurbhiAgarwal1 please fix the build issues. I'll get back to reviewing it after the build and tests are passing.

Copilot AI review requested due to automatic review settings February 19, 2026 07:59
Copy link

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings February 19, 2026 10:57
@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from 155590a to fc9326d Compare February 19, 2026 11:06
Copy link

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 encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Feb 20, 2026
Copy link

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.

@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from cf1b03a to 4ccfe93 Compare February 26, 2026 09:25
Copilot AI review requested due to automatic review settings February 26, 2026 09:38
Copy link

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.

Copilot AI review requested due to automatic review settings February 26, 2026 16:58
@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from 04e12f6 to 588479d Compare February 26, 2026 16:58
Copy link

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.

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Feb 26, 2026
@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch 2 times, most recently from 1af8cd4 to d2288d6 Compare February 26, 2026 17:08
Copilot AI review requested due to automatic review settings February 26, 2026 17:11
@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from d2288d6 to 588479d Compare February 26, 2026 17:11
@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Feb 26, 2026
Copy link

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.

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Feb 26, 2026
Signed-off-by: Surbhi <agarwalsurbhi1807@gmail.com>
Copilot AI review requested due to automatic review settings February 27, 2026 06:23
@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from ef74d9f to fa984be Compare February 27, 2026 06:23
@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Feb 27, 2026
Copy link

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 6 out of 7 changed files in this pull request and generated 3 comments.


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

Comment on lines +58 to +60
// Create the program with cost tracking to prevent resource exhaustion
// This enforces a runtime cost limit similar to Kubernetes ValidatingAdmissionPolicy
prg, err := env.Program(ast, cel.CostTracking(nil))
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The CEL cost tracking is enabled but with a nil limit, which means there's no actual runtime cost limit enforced. According to the comment, this should prevent resource exhaustion similar to Kubernetes ValidatingAdmissionPolicy, but a nil cost tracker doesn't enforce any limit. Consider using cel.CostLimit to set an actual cost limit, or if unlimited evaluation is intentional, the comment should be updated to reflect that cost tracking is enabled for monitoring purposes only without enforcement.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 27, 2026 06:53
@SurbhiAgarwal1 SurbhiAgarwal1 force-pushed the feat/cel-conditional-execution branch from 6b864d3 to fa984be Compare February 27, 2026 06:58
Copy link

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 6 out of 7 changed files in this pull request and generated 3 comments.


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

Comment on lines +32 to +40
// NewCELEvaluator creates a new CEL evaluator with the standard environment
// The environment is created once and reused for all evaluations
func NewCELEvaluator(condition string) (*CELEvaluator, error) {
env, err := cel.NewEnv(
cel.Variable("resources", cel.ListType(cel.DynType)),
)
if err != nil {
return nil, fmt.Errorf("failed to create CEL environment: %w", err)
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The doc comment claims “The environment is created once and reused for all evaluations”, but NewCELEvaluator currently creates a new cel.Env on every call. Either adjust the comment to reflect per-evaluator creation, or make the env a shared singleton if reuse is intended.

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +239

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

if !shouldExecute {
if !fr.disableCLIOutput {
pr.Printf("[SKIPPED] %q (condition not met)\n", fr.name)
}
// Return input unchanged - function is skipped
return input, nil
}
}

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

When a function is skipped due to a false condition, Filter returns early without updating fr.fnResult / appending to fr.fnResults.Items. This makes the structured FunctionResultList omit the function entirely (unlike the normal do() path, which always appends a Result), which can break consumers that expect one result per pipeline step. Consider appending a Result with ExitCode=0 (and optionally a structured message indicating it was skipped) before returning.

Suggested change
// Check condition before executing function
if fr.evaluator != nil {
shouldExecute, err := fr.evaluator.EvaluateCondition(fr.ctx, input)
if err != nil {
return nil, fmt.Errorf("failed to evaluate condition for function %q: %w", fr.name, err)
}
if !shouldExecute {
if !fr.disableCLIOutput {
pr.Printf("[SKIPPED] %q (condition not met)\n", fr.name)
}
// Return input unchanged - function is skipped
return input, nil
}
}
// Check condition before executing function
if fr.evaluator != nil {
shouldExecute, err := fr.evaluator.EvaluateCondition(fr.ctx, input)
if err != nil {
return nil, fmt.Errorf("failed to evaluate condition for function %q: %w", fr.name, err)
}
if !shouldExecute {
if !fr.disableCLIOutput {
pr.Printf("[SKIPPED] %q (condition not met)\n", fr.name)
}
// Record a synthetic successful result for the skipped function so that
// consumers that expect one result per pipeline step still see an entry.
skippedResult := &fnresult.Result{
ExitCode: 0,
}
fr.fnResult = skippedResult
if fr.fnResults != nil {
fr.fnResults.Items = append(fr.fnResults.Items, skippedResult)
}
// Return input unchanged - function is skipped
return input, nil
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +224 to +228
// Check condition before executing function
if fr.evaluator != nil {
shouldExecute, err := fr.evaluator.EvaluateCondition(fr.ctx, input)
if err != nil {
return nil, fmt.Errorf("failed to evaluate condition for function %q: %w", fr.name, err)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The condition is evaluated against the input passed into FunctionRunner.Filter. In the render/hydration flow, inputs are often pre-filtered via SelectInput (selectors/exclusions) before calling Filter, so this ends up evaluating the condition against only the selected subset rather than the full package resources (contradicting the schema comment and issue intent). Consider evaluating conditions at a higher level where the full resource set is available, or changing the call pattern so Filter receives the full resource list for condition evaluation.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/fn-runtime KRM function runtime size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants