Feat: give filters and actions what changed in an update
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:
| Case | In changed |
|---|---|
| Field added or changed | its new value |
Field removed (or set to null) | null |
| Anything inside a list changed | the whole new list |
| create / delete | changed 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, overevanphx/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 stayint64. - Queue items are now
*InformerEvent: the workqueue keys its items, and a struct with a map field cannot be a key. EventHandlertakes a singlePayload{SourceType, Event, Data, Changed}instead of positional arguments; the cronjob source andAddHandlerBefore/Afterare updated.
Why not pass the old object too
Measured on a 5 KB Deployment, per trigger per update:
| Filter context | Time |
|---|---|
data (today) | 1.7 ms |
+ changed | 1.8 ms |
+ changed + whole old object | 3.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.changedand how to test it. - Run
make reviewableto ensure this PR is ready for review. (Ranmake lint,make checklicenseandgo mod tidy; notgenerate, as the localcontroller-gendiffers from the pinned v0.9.0.) - Added
backport release-x.ylabels 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 testsfalseinstead 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. EventHandlernow takes a singlePayload(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 yieldsnull.
Bug Fixes
- Updates with an unchanged
resourceVersionare 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.