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

BR
15d ago
+788-20

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:

ScopeHow
Standalone kube-trigger--enable-cel-expressions=false
One TriggerServiceannotation trigger.oam.dev/cel-expressions: "false"
Every TriggerServicemanager --enable-cel-expressions=false
Opt-in onlymanager --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

  • Independent of #61, #62 and #63. With #63, context.changed becomes readable from CEL with no change here; whichever merges second needs a small rebase in the event handler.
  • context is evaluated dynamically. Typed checking against the watched kind's schema (as ValidatingAdmissionPolicy does) is a possible follow-up; it needs a TriggerService admission webhook, which kube-trigger does not have.

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.