Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/standard-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ jobs:
# `bash -e`, so a bare test returning false on the last iteration would
# abort the script before the ::error:: line ever printed.
run: |
# ubuntu-latest pre-bakes google-chrome.list (and, on current images, the deb822 google-chrome.sources) into the apt sources, so
# every `apt-get update` here depends on dl.google.com serving
# consistent index metadata. On 2026-09-09 it did not ("Hash Sum
# mismatch", ~10 minutes) and all three attempts below lost on main for
# a repository this job never reads. Drop it before the first update.
sudo rm -f /etc/apt/sources.list.d/google-chrome*
for i in 1 2 3; do
if sudo timeout 60 apt-get update -qq \
&& sudo timeout 60 apt-get install -y bats; then
Expand Down
4 changes: 2 additions & 2 deletions client/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: client
description: A unified Helm chart for tracebloc on AKS, EKS, bare-metal, and OpenShift
type: application
version: 1.9.110
appVersion: "1.9.110"
version: 1.9.112
appVersion: "1.9.112"
keywords:
- tracebloc
- kubernetes
Expand Down
2 changes: 1 addition & 1 deletion client/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{{ "\033[1;34m" }}Service Account:{{ "\033[0m" }} {{ "\033[0;33m" }}{{ include "tracebloc.serviceAccountName" . }}{{ "\033[0m" }}
{{ "\033[1;34m" }}Secret:{{ "\033[0m" }} {{ "\033[0;33m" }}{{ include "tracebloc.secretName" . }}{{ "\033[0m" }}
{{ "\033[1;34m" }}RBAC scope:{{ "\033[0m" }} {{ "\033[0;33m" }}{{ if ne .Values.clusterScope false }}Cluster{{ else }}Namespace{{ end }}{{ "\033[0m" }}
{{ "\033[1;34m" }}Image registry:{{ "\033[0m" }} {{ "\033[1;32m" }}docker.io{{ "\033[0m" }}
{{ "\033[1;34m" }}Image registry:{{ "\033[0m" }} {{ "\033[1;32m" }}{{ include "tracebloc.tbRegistry" . }}{{ "\033[0m" }} (tracebloc control-plane images)
{{- if (default dict .Values.hostPath).enabled }}
{{ "\033[1;34m" }}Storage:{{ "\033[0m" }} {{ "\033[0;33m" }}hostPath (bare-metal){{ "\033[0m" }}
{{ "\033[1;34m" }}Host dirs:{{ "\033[0m" }} {{ "\033[0;33m" }}/tracebloc/data, /tracebloc/logs, /tracebloc/mysql (on the node){{ "\033[0m" }}
Expand Down
80 changes: 74 additions & 6 deletions client/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -463,10 +463,12 @@ true
the kubelet pulls a digest it has never seen. Two conditions:
* the CronJob renders at all (`imageRefresh.enabled`, and not every
refreshed image already pinned), and
* images come from docker.io. Under a `global.imageRegistry` mirror the
script goes inert by design — it resolves digests from docker.io, and
pinning one onto a mirrored reference could pin an image the mirror
does not hold.
* the images come from a registry the script can resolve digests on
anonymously (tracebloc.imageRefreshResolvableRegistries: docker.io,
ghcr.io — wherever tracebloc.tbRegistry points). Under a
`global.imageRegistry` mirror the script goes inert by design — it
cannot resolve there, and pinning a digest resolved elsewhere onto a
mirrored reference could pin an image the mirror does not hold.
3. Otherwise -> Always. No reconcile, no pin, so a floating tag plus a
restart is the ONLY way that edge can ever move. This is exactly the
pre-#569 behaviour, kept for exactly the edges that still depend on it:
Expand All @@ -483,10 +485,9 @@ true
Usage: {{ include "tracebloc.controlPlanePullPolicy" (dict "digest" $d "root" $) }}
*/}}
{{- define "tracebloc.controlPlanePullPolicy" -}}
{{- $mirror := (dig "imageRegistry" "docker.io" (.root.Values.global | default dict)) | default "docker.io" -}}
{{- if .digest -}}
IfNotPresent
{{- else if and (include "tracebloc.imageRefreshEnabled" .root) (eq $mirror "docker.io") -}}
{{- else if and (include "tracebloc.imageRefreshEnabled" .root) (include "tracebloc.imageRefreshResolvable" .root) -}}
IfNotPresent
{{- else -}}
Always
Expand Down Expand Up @@ -584,6 +585,73 @@ with the ROOT context (e.g. `include "tracebloc.mirrorPrefix" $`).
{{- with (dig "imageRegistry" "" (.Values.global | default dict)) }}{{ . }}/{{ end -}}
{{- end -}}

{{/*
tracebloc.imageRefreshResolvableRegistries — the registries the image-refresh
script can resolve a floating tag to a digest on WITHOUT a credential: the
public token endpoints it knows (`get_token` / `get_latest_digest` in
image-refresh-cronjob.yaml carry one arm per entry). Space-separated, ONE
declaration: `tracebloc.imageRefreshResolvable` (below) and the
`tracebloc.controlPlanePullPolicy` decision both read it, and the CronJob
renders the verdict into the script's env as IMAGE_REGISTRY_RESOLVABLE — so the
pods' pull policy and the script's "can I reconcile here" guard cannot disagree.
Adding a registry here without a matching token arm in the script would make
the pods IfNotPresent while every tick WARNs "unknown registry"; the unit tests
pin the arms to this list.
*/}}
{{- define "tracebloc.imageRefreshResolvableRegistries" -}}
docker.io ghcr.io
{{- end -}}

{{/*
tracebloc.tbRegistry — the registry the tracebloc-PUBLISHED control-plane images
(tracebloc/jobs-manager, tracebloc/pods-monitor, tracebloc/resource-monitor, and
the requests-proxy, which runs the jobs-manager image) are pulled from.

ONE precedence chain, so the four call sites, the image-refresh CronJob and
NOTES.txt cannot disagree about where those images live:

1. `global.imageRegistry` — a private mirror re-homes EVERY image the
chart pulls (#585), tracebloc/* included.
It always wins.
2. `images.traceblocRegistry` — the tracebloc-only knob: moves just the
tracebloc-published images, leaving busybox,
squid, alpine/*, the device plugins and the
ingestor where they are. Also the per-edge
rollback: set it to the previous registry.
3. "docker.io" — the chart default while the images are
published to Docker Hub.

NOT routed through here, on purpose: `tracebloc/mysql-client` (frozen,
digest-pinned, published only to Docker Hub — see images.mysqlClient), the
third-party images (each has its own `registry` key), and — for now — the
training-image host JOB_IMAGE_HOST, which moves in its own step.

Every read is nil-guarded and `| default`-chained: values.yaml ships
`global.imageRegistry: ""` (the key EXISTS, so `dig`'s own fallback never
applies — the trap image_refresh_test.yaml pins), and an edge upgrading with
`--reuse-values` from before `images.traceblocRegistry` existed has no such key
at all. Both must render the default, never "".

Call with the ROOT context: {{ include "tracebloc.tbRegistry" . }}
*/}}
{{- define "tracebloc.tbRegistry" -}}
{{- $mirror := dig "imageRegistry" "" (.Values.global | default dict) -}}
{{- $own := dig "traceblocRegistry" "" (.Values.images | default dict) -}}
{{- $mirror | default ($own | default "docker.io") -}}
{{- end -}}

{{/*
tracebloc.imageRefreshResolvable — "true" when the image-refresh script can
reconcile on this edge: the registry tracebloc.tbRegistry resolves to is one of
tracebloc.imageRefreshResolvableRegistries. Empty otherwise (a private mirror,
or a registry the script has no token arm for). Call with the ROOT context.
*/}}
{{- define "tracebloc.imageRefreshResolvable" -}}
{{- if has (include "tracebloc.tbRegistry" .) (splitList " " (include "tracebloc.imageRefreshResolvableRegistries" .)) -}}
true
{{- end -}}
{{- end -}}

{{/*
tracebloc.ingestorDigest — the ONE effective digest for the spawned ingestor
image. Renders the digest to pin to, or nothing at all to float on
Expand Down
2 changes: 1 addition & 1 deletion client/templates/egress-proxy-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ spec:
resources:
requests:
cpu: {{ $epReq.cpu | default "50m" | quote }}
memory: {{ $epReq.memory | default "64Mi" | quote }}
memory: {{ $epReq.memory | default "160Mi" | quote }}
limits:
cpu: {{ $epLim.cpu | default "500m" | quote }}
memory: {{ $epLim.memory | default "256Mi" | quote }}
Expand Down
77 changes: 46 additions & 31 deletions client/templates/image-refresh-cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ data:
image-refresh.sh: |
#!/bin/sh
# Polls the registry for a new manifest digest and reconciles it onto the
# long-lived control-plane workloads, on docker.io under the floating
# CLIENT_ENV tag (#154, extended by #569).
# long-lived control-plane workloads, on IMAGE_REGISTRY (the registry the
# chart pulls the tracebloc-published images from -- see the
# tracebloc.tbRegistry helper) under the floating CLIENT_ENV tag (#154,
# extended by #569).
#
# Action on change: `kubectl set image <workload> <container>=repo@digest`.
# It used to be `kubectl rollout restart`. #569 changed the mechanism so
Expand Down Expand Up @@ -180,16 +182,24 @@ data:
SKIP_KEY="tracebloc.io/refresh-skip-streak"

log "release=$RELEASE_NAME namespace=$RELEASE_NAMESPACE deployment=$DEPLOYMENT_NAME"
log " client images: tracebloc/{jobs-manager,pods-monitor,resource-monitor} on docker.io under tag=$IMAGE_TAG"
log " client images: tracebloc/{jobs-manager,pods-monitor,resource-monitor} on ${IMAGE_REGISTRY} under tag=$IMAGE_TAG"
log " also reconciling: deployment/${REQUESTS_PROXY_DEPLOYMENT} (jobs-manager image), daemonset/${RESOURCE_MONITOR_DAEMONSET} in ${NODE_AGENTS_NAMESPACE}"

# #569: a private-mirror install (global.imageRegistry) renders every pod
# image as <mirror>/tracebloc/... , but the digests resolved below come from
# docker.io. Writing a docker.io digest onto a mirror reference is not just
# wrong, it is BREAKING: the mirror may not hold that digest at all, so
# `set image` would pin the control plane to an unpullable ref. Under the
# old `rollout restart` this mismatch was merely useless (it restarted pods
# to re-pull the mirror's own tag); with `set image` it must fail closed.
# image as <mirror>/tracebloc/... , but this script can only resolve a tag
# to a digest on the public registries it has a token arm for (get_token
# below: docker.io, ghcr.io). Writing a digest resolved elsewhere onto a
# mirror reference is not just wrong, it is BREAKING: the mirror may not
# hold that digest at all, so `set image` would pin the control plane to an
# unpullable ref. Under the old `rollout restart` this mismatch was merely
# useless (it restarted pods to re-pull the mirror's own tag); with
# `set image` it must fail closed.
#
# The verdict is RENDERED by the chart (IMAGE_REGISTRY_RESOLVABLE, from the
# same tracebloc.imageRefreshResolvableRegistries declaration that decides
# the pods' imagePullPolicy) rather than re-derived here, so the guard and
# the pull policy cannot disagree about which edges the reconcile runs on.
# Absent or anything but "true" is refused: not knowing is not permission.
#
# Exit 0, not 1: a mirror install has deliberately taken over image
# distribution, so "nothing to reconcile" is the correct steady state, not
Expand All @@ -198,9 +208,9 @@ data:
# The CronJob is still RENDERED under a mirror (the alpine/k8s runner image
# is re-homed onto it, per global_image_registry_test.yaml); only the
# reconcile is inert.
if [ "$IMAGE_REGISTRY" != "docker.io" ]; then
log "NOTE: images are served from private mirror '${IMAGE_REGISTRY}' (global.imageRegistry), but this script resolves digests from docker.io."
log " Pinning a docker.io digest onto a mirrored reference could pin an image the mirror does not hold, so auto-refresh is INERT on this edge."
if [ "${IMAGE_REGISTRY_RESOLVABLE:-}" != "true" ]; then
log "NOTE: images are served from '${IMAGE_REGISTRY}', a registry this script cannot resolve digests on anonymously (it knows docker.io and ghcr.io)."
log " Pinning a digest resolved elsewhere onto that reference could pin an image the registry does not hold, so auto-refresh is INERT on this edge."
log " Update the control-plane images by syncing your mirror and restarting the workloads, or pin images.<image>.digest explicitly."
log " (That restart DOES re-pull: because this reconcile is inert here, the chart renders imagePullPolicy=Always on the unpinned control-plane images rather than IfNotPresent -- see the tracebloc.controlPlanePullPolicy helper. Offline-restart safety is traded away on this edge precisely so it is not left with no update path at all.)"
log "tick complete"
Expand Down Expand Up @@ -547,7 +557,7 @@ data:
# it changes no behaviour: `continue` still ends this image's tick either
# way. Only the reporting is new.
stale_key="tracebloc.io/stale-pin-${repo#*/}"
pin_latest="$(get_latest_digest "$repo" "$IMAGE_TAG" "docker.io" || true)"
pin_latest="$(get_latest_digest "$repo" "$IMAGE_TAG" "$IMAGE_REGISTRY" || true)"
if [ -z "$pin_latest" ]; then
log " pinned by digest in values; skipping (could not resolve ${IMAGE_TAG} to compare -- not knowing is a finding, not agreement)"
log " leaving any existing ${stale_key} as the last KNOWN state rather than asserting agreement"
Expand Down Expand Up @@ -587,7 +597,7 @@ data:
annotate_args="$annotate_args ${unpinned_stale_key}-"
fi

latest="$(get_latest_digest "$repo" "$IMAGE_TAG" "docker.io" || true)"
latest="$(get_latest_digest "$repo" "$IMAGE_TAG" "$IMAGE_REGISTRY" || true)"
if [ -z "$latest" ]; then
log " WARN: could not resolve latest digest (rate-limited or transient); skipping this tick"
continue
Expand Down Expand Up @@ -726,9 +736,10 @@ data:
annotate_args="$annotate_args ${key}=${latest}"
restart_needed=1

# The reference we pin. IMAGE_REGISTRY is docker.io here — a mirror
# exits the tick far above — but render it from the variable so this
# stays honest if that guard is ever relaxed.
# The reference we pin: the SAME registry the digest above was resolved
# on, so a copy of this image on another registry can never be pinned by
# a digest it does not hold. A registry the script cannot resolve on
# exited the tick far above (IMAGE_REGISTRY_RESOLVABLE).
ref="${IMAGE_REGISTRY}/${repo}@${latest}"

# Map the image onto the workload+container(s) that run it. Container
Expand Down Expand Up @@ -990,21 +1001,25 @@ spec:
value: {{ include "tracebloc.resourceMonitorName" . | quote }}
- name: NODE_AGENTS_NAMESPACE
value: {{ .Values.nodeAgents.namespace.name | quote }}
# The registry the chart renders into every pod image. The
# script resolves digests from docker.io only, so it goes inert
# when this is a private mirror rather than pinning a digest the
# mirror may not hold (see the guard at the top of the script).
#
# `| default "docker.io"` is NOT redundant with dig's default:
# values.yaml ships `global.imageRegistry: ""`, so the key EXISTS
# and dig returns the empty string rather than its fallback.
# Without the trailing default this renders "", the mirror guard
# below sees "" != "docker.io" and goes inert on every DEFAULT
# install — silently disabling refresh fleet-wide. The
# `tracebloc.image` helper handles the same trap with its own
# `| default "docker.io"`.
# The registry the chart renders into the control-plane pod
# images -- ONE helper (tracebloc.tbRegistry: global mirror, else
# images.traceblocRegistry, else the chart default), so the
# script resolves digests on exactly the registry the pods pull
# from. The helper owns the `| default` chain: values.yaml ships
# `global.imageRegistry: ""`, so the key EXISTS and a bare dig
# returns "" rather than its fallback; rendering "" here would
# make the reconcile go inert on every DEFAULT install (the
# "IMAGE_REGISTRY defaults to ..., not the empty string" test).
- name: IMAGE_REGISTRY
value: {{ dig "imageRegistry" "docker.io" (.Values.global | default dict) | default "docker.io" | quote }}
value: {{ include "tracebloc.tbRegistry" . | quote }}
# Whether the script can resolve digests on IMAGE_REGISTRY
# anonymously -- derived from the same declaration
# (tracebloc.imageRefreshResolvableRegistries) that switches the
# pods to IfNotPresent, so the guard at the top of the script and
# the pull policy agree by construction. "false" makes the tick
# log and exit 0 (a mirror install has taken over distribution).
- name: IMAGE_REGISTRY_RESOLVABLE
value: {{ include "tracebloc.imageRefreshResolvable" . | default "false" | quote }}
- name: IMAGE_TAG
# RESOLVED, not raw. This is the tag the refresh pulls, so a
# documented alias here freezes the edge on a tag the publish
Expand Down
Loading
Loading