From 36134d7140785f123e93b8e03cc64210dcf8fcf2 Mon Sep 17 00:00:00 2001 From: Asad Iqbal Date: Mon, 25 May 2026 13:34:42 +0500 Subject: [PATCH] fix(#154): read annotations via jq, not kubectl jsonpath bracket notation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught in dev-cluster smoke test on tb-client-dev-templates. `kubectl get -o jsonpath="{.metadata.annotations['key.with.dots']}"` returns empty for keys containing `.` or `/` on kubectl-go 1.30.x — verified directly: `kubectl get ... -o jsonpath='{.metadata.annotations}'` showed `tracebloc.io/last-refreshed-jobs-manager-digest:sha256:f913...` present and persisted, but the script's bracket-notation read returned empty. Effect: every tick saw `recorded=` and re-entered the first-observation path, re-annotating the deployment with the same digest on every run. No spurious restarts (first-observation skips restart), but the script was incapable of ever transitioning to the "unchanged; no-op" path, and a real image update would have been indistinguishable from first observation. Fix: read via `kubectl get -o json | jq -r --arg k "$_key" '.metadata.annotations[$k] // empty'`. alpine/k8s ships jq, so this adds no new dependency. The dot-escape jsonpath form (`.tracebloc\.io/...`) also works but is fragile against future kubectl version changes; jq's behaviour is locked. Regression tests: * The script must include `jq -r --arg k` (positive match). * The script must NOT include `annotations['$_key']` bracket-notation jsonpath (negative match — the regression vector itself). Co-Authored-By: Claude Opus 4.7 --- client/templates/image-refresh-cronjob.yaml | 17 +++++++++++++---- client/tests/image_refresh_test.yaml | 13 +++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/client/templates/image-refresh-cronjob.yaml b/client/templates/image-refresh-cronjob.yaml index 94d8e7b..c1a15a1 100644 --- a/client/templates/image-refresh-cronjob.yaml +++ b/client/templates/image-refresh-cronjob.yaml @@ -92,12 +92,21 @@ data: # Read an annotation value off the deployment. Empty output means # the annotation is absent (first observation) — distinct from an # explicit empty string, which the chart never writes. + # + # Uses jq because kubectl-go's jsonpath parser returns empty for + # bracket-notation keys that contain `.` or `/` — verified on + # alpine/k8s 1.30.5 against a real annotation with key + # `tracebloc.io/last-refreshed-jobs-manager-digest`. Dot-escape + # syntax (`.tracebloc\.io/...`) is the other working form, but jq + # is unambiguous and won't surprise us on future kubectl versions. + # alpine/k8s ships jq, so adding one jq call doesn't pull a new + # dependency (the script remains awk/sed/grep elsewhere; jq is + # used ONLY for this read because JSON-with-dotted-keys is what + # actually motivates jq's existence). get_annotation() { _key="$1" - # jsonpath treats '.' / '/' as separators; brace-escape the key. - kubectl get deployment -n "$RELEASE_NAMESPACE" "$DEPLOYMENT_NAME" \ - -o "jsonpath={.metadata.annotations['$_key']}" \ - | tr -d '\r\n' + kubectl get deployment -n "$RELEASE_NAMESPACE" "$DEPLOYMENT_NAME" -o json \ + | jq -r --arg k "$_key" '.metadata.annotations[$k] // empty' } restart_needed=0 diff --git a/client/tests/image_refresh_test.yaml b/client/tests/image_refresh_test.yaml index 243a87b..fcfbc4b 100644 --- a/client/tests/image_refresh_test.yaml +++ b/client/tests/image_refresh_test.yaml @@ -148,6 +148,19 @@ tests: - matchRegex: path: data["image-refresh.sh"] pattern: "tracebloc\\.io/last-refreshed-pods-monitor-digest" + # Regression guard: read annotations with jq, not kubectl + # bracket-notation jsonpath. kubectl-go's jsonpath parser returns + # empty for `annotations['key.with.dots']` — verified on + # alpine/k8s 1.30.5 in dev-cluster smoke test. Switching to + # `kubectl get -o json | jq` made the read work. + - matchRegex: + path: data["image-refresh.sh"] + pattern: "jq -r --arg k" + # And the script must NOT regress back to bracket-notation + # jsonpath for reading annotations. + - notMatchRegex: + path: data["image-refresh.sh"] + pattern: "annotations\\[.\\$_key.\\]" # The script must NOT reach into pod containerStatuses to read # imageIDs — that's the runtime-variant data shape we explicitly # designed around. Pattern targets the only call shape that could