Feat: give filters and actions what changed in an update

BR
1mo ago
+847-93

Description of your changes

Why

A filter only ever saw the object after an update, so a trigger could not decide when to run based on what changed: "the spec changed", not a status write; "a label was added"; "the phase became Running".

What

context.changed is the JSON merge patch from the old object to the new one, set only on a resource-watcher update:

CaseIn changed
Field added or changedits new value
Field removed (or set to null)null
Anything inside a list changedthe whole new list
create / deletechanged is absent

A CUE path test is false, not an error, when the path is missing, so filters need no guard even on triggers that watch every event type:

filter: context.changed.spec != _|_                          # spec changed, skip status writes
filter: context.changed.metadata.labels != _|_               # any label added, changed or removed
filter: (context.changed.status.phase & "Running") != _|_    # phase became Running

The last form unifies rather than compares, because == on a missing path is an error in CUE and would be logged for every other update. The README documents all of this, plus the gotcha that removing a map's only key drops the map (labels: null).

How

  • The watcher diffs in UpdateFunc (utils.MergePatch, over evanphx/json-patch/v5, now a direct dependency), so the queue holds the patch rather than a second copy of the object. Numbers are decoded as unstructured objects are, so they stay int64.
  • Queue items are now *InformerEvent: the workqueue keys its items, and a struct with a map field cannot be a key.
  • EventHandler takes a single Payload{SourceType, Event, Data, Changed} instead of positional arguments; the cronjob source and AddHandlerBefore/After are updated.

Why not pass the old object too

Measured on a 5 KB Deployment, per trigger per update:

Filter contextTime
data (today)1.7 ms
+ changed1.8 ms
+ changed + whole old object3.3 ms

Filters and actions encode the whole context into CUE, so a second full object doubles their cost. changed covers deciding when to run; the old object can be added later without breaking anything if a use needs old values (direction of a change, "left state X").

I have:

  • Read and followed KubeVela's pull request process.
  • All commits are signed off (DCO).
  • Related Docs updated properly. kube-trigger documents filters in its README, which covers context.changed and how to test it.
  • 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 v0.9.0.)
  • 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: MergePatch (integers stay exact, including beyond 2^53; removed label; whole lists; unchanged object), the update handler attaching changed, an end-to-end watcher test against a fake dynamic client, buildContext, and the README's filter forms through ApplyFilter. go test ./pkg/... passes, and the changed packages pass under -race. Not yet run against a live cluster.

Special notes for your reviewer

  • Stacked on #62. Only the last commit (Feat: give filters and actions what changed in an update) is new here; I'll rebase once #62 merges.

Summary by cubic

Gives resource-watcher filters and actions the diff between the old and new object on updates, so triggers can decide when to run based on what changed instead of only seeing the post-update state.

New Features

  • Adds context.changed, a JSON merge patch (RFC 7386) of the fields that differ, set only on updates; it's absent on create and delete, and a missing CUE path tests false instead of erroring.
  • The watcher diffs in its update handler and queues the patch rather than the old object, which would double filter and action cost; numbers stay int64, and queue items are pointers since the queue keys them.
  • EventHandler now takes a single Payload (source type, event, data, changed) instead of positional arguments.
  • README documents context.changed, including how to test paths and the gotcha that removing a map's only key yields null.

Bug Fixes

  • Updates with an unchanged resourceVersion are skipped when one is known to compare, so relists no longer re-run every trigger for untouched resources.
  • Delete events arriving as tombstones (a missed watch noticed on relist) no longer panic.
  • Each trigger only receives the event types it asked for when triggers share a watcher, whose merged listen covers every event any trigger requested.
  • Each controller keeps its own start time, so one watcher's start no longer drops another's real creates.