Themelio compatibility reference. This page describes the retained Themelio implementation or its historical design. Check the parallel-operation guide before applying it to the current platform.
DD-0019: Container Image Tag Management — Implementation
Related ADR: ADR-0019: Container Image Tag Management for GitOps Deployments
Status: Draft
Date: 2026-06-29
Authors: TBD
ADR-derived technical notes
These notes capture implementation detail surfaced while scoping ADR-0019, kept for the chosen direction (Option B direct-to-Application + Option R1 retention). Stale per-option notes (the rejected Options A/D git write-back, and the incorrect "GitLab Container Registry cleanup policy") have been removed.
- Re-enabling immutable tags: the per-commit tags already existed and were commented out in
themelio.config.yaml(commite2b0ecaa):dev-${GIT_COMMIT},staging-${GIT_COMMIT},prod-${GIT_COMMIT}.GIT_COMMITis already short —scripts/build.shsets it viagit rev-parse --short HEAD— and tags are expanded withenvsubst, which does not support a bash substring (${GIT_COMMIT:0:8}). So re-enabling is simply uncommenting the existing lines; no substring expression. - Pinning standardization: all services converge on a hardcoded tag in
deployment.yaml. No git-side kustomizeimages:block is needed — Image Updater'sspec.source.kustomize.imagesoverride matches by image name and replaces the tag regardless.waldur-site-agent-cloud's staging overlay (which previously used animages:block) was converted to the same hardcoded-tag patch as its dev/prod overlays. - ArgoCD specifics: root apps are
gitops/<env>-root-app.yamlwithautomated: { prune: true, selfHeal: true },repoURLthe same repo,path: gitops/apps/overlays/<env>,targetRevision: HEAD. The app-of-apps renders the child Applications from git, which is whyselfHealwould otherwise scrub a direct image override (see Design §4). - No git write-back / no same-repo loop: because the chosen write-back is direct-to-Application, nothing is committed to git, so the same-repo CI-loop concern (Q2) and a scoped bot token are not needed.
- Registry is
twuni/docker-registry(plain CNCF Distribution): deployed byregistry-values.yml.j2. It has no native "keep last N" retention — that capability is Harbor/GitLab-registry only. Today the template sets neitherpersistence.deleteEnabled(DELETE API returns 405) norgarbageCollect.enabled(no GC CronJob), so old images accumulate. Distribution can only delete by digest and bulk---delete-untagged; bounded retention therefore requires an external prune over enumerable tags (Design §5). newest-buildon Distribution: Image Updater'snewest-buildstrategy ranks tags by image build timestamp, read from each image's config blob; this works on plain Distribution (no Harbor API needed).
Overview
This document specifies how Themelio deploys freshly built service images without manual kubectl, and how it bounds registry growth. CI tags each build with an immutable <env>-<shortsha> tag and pushes it to the twuni/docker-registry registry. ArgoCD Image Updater watches the registry with the newest-build strategy and writes the selected tag directly onto the live ArgoCD Application (write-back-method: argocd); an ignoreDifferences carve-out on the parent root-app lets selfHeal manage every manifest field from git except the image version, which Image Updater owns. All environments auto-update. A daily Kubernetes CronJob prunes the registry to the newest 5 per service and the chart's GC reclaims the freed blobs; the prune emits structured JSON log lines that a Loki-backed Grafana dashboard renders as the active + retained tags per image (no Prometheus/Pushgateway component).
Goals and Non-Goals
Goals
- A CI-pushed build reaches its environment automatically, with no manual rollout.
- Every manifest field except the image version is reproducible from git and self-healed.
- Registry storage is bounded to the newest 5 tags per service, deep enough for a 4-build rollback.
- Operators can see, in Grafana, the active tag and the 4 retained predecessors per image.
Non-Goals
- Git-reproducibility or
git revertrollback of the image version (explicitly waived in the ADR; image rollback is a re-point via Image Updater/ArgoCD). - Migrating to a registry with native retention (Harbor/GitLab) — rejected as Option R3.
- Per-service (single-image) CI builds — removed from ADR-0019 scope.
- Any data migration — this is purely deploy-wiring.
Design
1. Build & tag (CI)
Re-enable the commented immutable tags in themelio.config.yaml so each build pushes both:
- the mutable env tag (
dev/staging/prod) — kept for convenience, not what drives deployment, and - the immutable
<env>-<shortsha>tag — the one Image Updater selects.
GIT_COMMIT is already short (git rev-parse --short HEAD in scripts/build.sh) and tags are expanded by envsubst, so the tags are just uncommented as-is — no substring expression. No change to scripts/build.sh's service loop. (Implemented.)
2. Pinning standardization
No git-side images: block is needed. Image Updater (write-back-method: argocd) writes the resolved tag into the Application's spec.source.kustomize.images, and the kustomize image transformer matches by image name — so it overrides the tag in each deployment.yaml/job-migrate.yaml regardless of the hardcoded :<env> value. The themelio overlays therefore keep their hardcoded tags as the git baseline; Image Updater overrides them at the Application level. waldur-site-agent-cloud's staging overlay was converted from its images: block to the same hardcoded-tag patch (matching its dev/prod overlays), so all services now standardize on hardcoded tags.
3. Image Updater (direct-to-Application)
The controller is installed in the argocd namespace from its upstream manifest by the argocd-deploy role (devops/ansible/roles/argocd-deploy/tasks/main.yml), alongside a registries.conf ConfigMap + basic-auth secret pointing at the pharos registries (reusing the k8s htpasswd user — pull-only).
The annotations live per-env in gitops/apps/overlays/<env>/kustomization.yaml, not apps/base, because the registry path differs per env (dev.registry…/test vs staging.registry…/themelio). They patch the single multi-image themelio Application:
write-back-method: argocd writes the resolved image into the live themelio Application's spec.source.kustomize.images — no git commit. All envs auto-update. (Implemented for dev + staging.)
4. selfHeal carve-out (Q4)
The child Applications are rendered from git by the parent gitops/<env>-root-app.yaml (selfHeal: true), which would scrub the override. Add ignoreDifferences on the parent for the child Application resource:
Result: git owns and self-heals every field except the image version.
5. Retention prune (Option R1)
Enable on the registry (in registry-values.yml.j2):
Add a daily CronJob in the registry namespace, deployed by the image-registry role (devops/ansible/roles/image-registry/templates/registry-prune.yml.j2) (ConfigMap script + CronJob). It uses regctl (not raw curl) with the registry's basic-auth creds (registry-auth secret, ci user). Per service it:
regctl repo ls→ repositories;regctl tag ls→ tags.- Skip the protected mutable tags (
dev/staging/prod/latest); rank the rest newest-first by image build timestamp (regctl image config). - Keep the newest 5;
regctl tag rm(DELETE manifest) for the rest. - Emit one structured JSON log line per service (
event=registry_prune,service,active,kept,pruned, counts).
The prune only untags; the chart GC CronJob reclaims the blobs (offline GC does not need deleteEnabled, but the prune's tag rm does — hence image_registry_delete_enabled: true).
6. Observability (Loki + Grafana)
No Prometheus/Pushgateway component. The prune CronJob's JSON log lines are shipped to the existing Loki stack. A Grafana dashboard (registry-image-tags.json), provisioned as a ConfigMap via the grafana_dashboard sidecar label, renders:
- a table of the latest prune per service (
activetag +keptretained set) via{namespace="registry", app="registry-prune"} | json, and - a logs panel of recent prune events.
Data Model
Tag scheme: <registry>/<name>:<env>-<shortsha> where <shortsha> is git rev-parse --short HEAD; immutable. Mutable <env> tags coexist but are protected from pruning.
Prune log line (one per service per run, JSON): event="registry_prune", service, total, kept_count, pruned_count, active (newest kept tag), kept (space-separated retained set), pruned, protected. This is the dashboard's data source — there is no Prometheus metric.
API Design
Registry access is via regctl over the Distribution HTTP API V2 (Basic auth):
regctl repo ls/regctl tag ls— enumerate repos and tags.regctl image config— read the build timestamp for ranking.regctl tag rm→DELETE /v2/<name>/manifests/<digest>— untag (requiresdeleteEnabled: true; returns405otherwise).
Testing Strategy
- Tag build: a pipeline run pushes
<env>-<shortsha>; confirm it appears intags/list. - Image Updater: push a newer tag; confirm the child Application's
spec.source.kustomize.imagesand the running Deployment update without a git commit. - selfHeal: force a parent sync; confirm the image override survives and a change to any other field is reverted to git.
- Prune dry-run: run with deletes disabled/logged-only; assert it keeps exactly 5 per service and never selects a mutable tag.
- Guard: assert a run with only
dev/staging/prodpresent deletes nothing. - Dashboard: after a run, the Grafana Loki table shows the active tag + retained set per image.
- Acceptance: registry size stops growing across N>5 builds; rollback to any of the last 4 tags succeeds via Image Updater.
Deployment & Rollout
Applied through the existing Ansible roles (no new bespoke pipeline):
image-registryrole —deleteEnabled+garbageCollect.*values, and the pruneCronJob(§5).- Re-enable the immutable tags in
themelio.config.yaml(§1). - Overlays keep their hardcoded image tags; no kustomize
images:block (§2). argocd-deployrole installs Image Updater +registries.conf; the apps overlays carry the per-env annotations; the root-apps carryignoreDifferences(§3–4).prometheus-grafana-deployrole auto-loads the Loki dashboard JSON (§6).- Verify per the Testing Strategy. All envs auto-update once wired.
Rollback of this change: revert the overlay/values commits; Image Updater and the prune CronJob are additive and can be scaled to zero / suspended without affecting running workloads.
Open Questions
Promotion model (ADR Q3): dev auto vs staging/prod gated?Resolved: all environments auto-update to the newest matching<env>-<sha>tag.Pushgateway vs Loki-only?Resolved: Loki-only — the dashboard is built from LogQL over the prune job's JSON logs; no Prometheus metric or Pushgateway.- Active-tag source: the dashboard infers "active" from the newest registry tag in the prune log. If the actually-deployed image (read from the ArgoCD Application) is wanted instead, that needs a separate source.
- prod environment: there is no
prodthemelio overlay ingitops/manifests/themelio/overlays/today, so dev + staging are wired but prod is not. Confirm whether prod is deployed via this gitops path before wiring it. - Validation: none of this has been run against a cluster — Image Updater's
registries.confcredential format, theignoreDifferencesbehaviour under the app-of-apps, and the dashboard's LogQL/transformations all need live verification.
References
- ADR-0019: Container Image Tag Management for GitOps Deployments
themelio.config.yaml— image build/tag definitionsregistry-values.yml.j2— registry Helm values- ArgoCD Image Updater — strategies and write-back methods
- Distribution registry HTTP API V2
Page source
Source snapshot · Revision f928834c8.