Skip to content

Feat: CEL filters, and $( ) expressions in action properties - #68

Open
briankane wants to merge 2 commits into
kubevela:mainfrom
briankane:feat/cel-expressions
Open

briankane wants to merge 2 commits into
kubevela:mainfrom
briankane:feat/cel-expressions

Conversation

@briankane

@briankane briankane commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Description of your changes

Adds CEL to kube-trigger, using the $( ) expression engine in kubevela/pkg/cel (kubevela/pkg#143), with one root, context: the same object CUE filters see, so CEL and CUE read the same paths.

Filters

A filter that is a single $( ) is CEL; any other filter is CUE, as before. No working CUE filter has that form, so this needs no switch.

filter: $(context.data.status.readyReplicas == context.data.spec.replicas)
filter: $(has(context.data.metadata.labels) && "tier" in context.data.metadata.labels && context.data.metadata.labels["tier"] == "web")

CEL's && and || skip what cannot change the result, so has() and in can guard fields only some objects carry, which CUE's non-short-circuiting && cannot. A result that is not a bool is an error.

Expressions in action properties

$( ) in action properties is rendered per event. An expression that is a whole property keeps its type; one inside text is joined in; $$( is a literal $(.

action:
  type: patch-resource
  properties:
    resource:
      apiVersion: core.oam.dev/v1beta1
      kind: Application
      name: $(context.data.metadata.labels["app"])
      namespace: $(context.data.metadata.namespace)

The job's ID still comes from the properties as written, so jobs from one trigger keep sharing it.

Switches

On by default, and can be turned off:

Scope How
Standalone kube-trigger --enable-cel-expressions=false
One TriggerService annotation trigger.oam.dev/cel-expressions: "false"
Every TriggerService manager --enable-cel-expressions=false
Opt-in only manager --require-cel-expression-opt-in; only TriggerServices annotated "true"

The manager passes its choice to each worker explicitly through the default worker template. The annotation is read when a TriggerService's worker is created.

Breaking change: a literal $( in action properties, such as a shell's $(date) or Kubernetes' $(VAR), is now written $$(. Such a property stops kube-trigger at start with an error naming the trigger and property and saying so. The triggerservice-image-update example is updated.

Validation

Every expression is planned when kube-trigger starts; one that does not compile stops it, naming the trigger and property, instead of failing on every event. Typing is dynamic (any kind can be watched): a typo or a wrong type shows at event time, where the event is dropped and the error logged.

I have:

  • Read and followed KubeVela's pull request process.
  • All commits are signed off (DCO).
  • Related Docs updated properly. kube-trigger documents filters and its flags in its README: CEL filters, expressions in action properties, the switches, and the flag reference.
  • Run make reviewable to ensure this PR is ready for review. (Ran make lint, make checklicense and go mod tidy; not generate, as the local controller-gen differs from the pinned version.)
  • Added backport release-x.y labels to auto-backport this PR if necessary. Not needed: kube-trigger has no release branches.

How has this code been tested

Unit tests at each layer, written before the code:

  • pkg/expression: telling CEL from CUE filters; filter results, has() and in guards, short-circuiting, a missing field and a non-bool result as errors; property rendering (typed whole values, interpolation, $$(); compile errors naming the property with the $$( hint.
  • pkg/eventhandler: CEL filters keep and drop events (including a test that fails if a CEL filter is read as CUE); bad expressions refuse the handler; properties are not read while expressions are off.
  • pkg/config: startup validation reports every bad expression with its trigger; every file in examples/ compiles with expressions on.
  • controllers/triggerservice: the switch and annotation decision table, and the default worker template rendering --enable-cel-expressions=true and =false.

go test ./pkg/... ./controllers/... passes; golangci-lint and staticcheck are clean. A CEL filter on a 5 KB Deployment evaluates in about 0.056 ms against about 1.05 ms for the CUE equivalent. Not yet run against a live cluster.

Special notes for your reviewer


Summary by cubic

Adds CEL expressions to kube-trigger using the expression engine from kubevela/pkg/cel. A filter that is a single $( ) is evaluated as CEL; any other filter is CUE as before. Both read the same context root, so CEL and CUE read the same paths.

  • $( ) in action properties is rendered per event; an expression that is a whole property keeps its type.
  • CEL's &&/|| short-circuit, so has() and in can guard fields only some objects carry; a non-bool filter result is an error.
  • Every expression is planned at startup, so one that does not compile stops kube-trigger naming the trigger and property, instead of failing per event. This includes a filter that opens with $( but never closes, which previously only failed per event.
  • Expression rendering in properties is on by default and can be turned off per worker, per TriggerService via the trigger.oam.dev/cel-expressions annotation, or for all TriggerServices; the manager can also require per-TriggerService opt-in.

Breaking change

  • A literal $( in action properties, such as a shell's $(date) or Kubernetes' $(VAR), must now be written $$(; an unescaped one stops kube-trigger at startup. The triggerservice-image-update example now uses backticks for the shell substitution, so it runs the same with expressions on or off.

Written for commit f6b173b. Summary will update on new commits.

View guided diff

Uses the $( ) expression engine in kubevela/pkg/cel, with one root,
context: the same object CUE filters see, so both read the same paths.

Filters: a filter that is a single $( ) is CEL; any other filter is CUE,
as before. No CUE filter has that form, so this needs no switch. CEL's
&& and || skip what cannot change the result, has() and `in` guard
fields only some objects carry, and a non-bool result is an error. A CEL
filter on a 5 KB Deployment evaluates in about 56 us against about
1.05 ms for the CUE equivalent.

Action properties: $( ) expressions over context are rendered per event;
one that is a whole property keeps its type, and $$( is a literal $(.
The job's ID still comes from the properties as written, so jobs from
one trigger keep sharing it.

Property expressions are on by default and can be turned off, since a
literal $( such as Kubernetes' $(VAR) or a shell's $(date) in a task's
command must now be written $$(; the compile error says so.

- The worker's --enable-cel-expressions (default true) turns them off.
- The manager's --enable-cel-expressions (default true) does so for
  every TriggerService, and passes its choice to each worker explicitly.
- A TriggerService annotated trigger.oam.dev/cel-expressions: "false"
  opts out; with the manager's --require-cel-expression-opt-in, only
  those annotated "true" render expressions.

Expressions are planned when kube-trigger starts: one that does not
compile stops it, naming the trigger and property, instead of failing on
every event.

The image-update example's task ran `echo ... $(date)`, which would now
stop kube-trigger at start; it is written $$(date). A test compiles every
shipped example with expressions on, and the README documents the flag in
its configuration reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGACRqRjRF3TqH6B9vVijh
Signed-off-by: Brian Kane <briankane1@gmail.com>
@codecov

codecov Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.00000% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.38%. Comparing base (3949320) to head (f6b173b).

Files with missing lines Patch % Lines
cmd/manager/main.go 0.00% 10 Missing ⚠️
pkg/cmd/cmd.go 0.00% 6 Missing ⚠️
pkg/expression/expression.go 87.23% 6 Missing ⚠️
pkg/eventhandler/event_handler.go 77.77% 4 Missing ⚠️
...ollers/triggerservice/triggerservice_controller.go 66.66% 3 Missing ⚠️
pkg/action/action.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #68      +/-   ##
==========================================
+ Coverage   45.39%   48.38%   +2.99%     
==========================================
  Files          31       32       +1     
  Lines        1454     1546      +92     
==========================================
+ Hits          660      748      +88     
- Misses        794      798       +4     
Flag Coverage Δ
unittests 48.38% <70.00%> (+2.99%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 18 files

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread pkg/eventhandler/event_handler_test.go Resolved
Comment thread pkg/expression/expression_test.go Resolved
Comment thread pkg/cmd/cmd.go Resolved
Comment thread pkg/expression/expression.go Resolved
Comment thread README.md Resolved
Review follow-ups.

- A filter that opens with $( but does not parse, such as one never
  closed, was taken for CUE and failed only when an event arrived. It is
  now an error at start, like any other CEL filter that does not compile.
- The image-update example writes the shell's command substitution with
  backticks rather than $$(date), so it runs the same with expressions
  on or off.
- The event handler test that a CEL filter keeps an event uses a fake
  client rather than recovering from any panic, so a panic in evaluation
  can no longer pass as a kept event.
- The README's environment variable for --enable-cel-expressions is
  ENABLE_CEL_EXPRESSIONS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGACRqRjRF3TqH6B9vVijh
Signed-off-by: Brian Kane <briankane1@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant