test(k8s): fail when compose and the Helm chart describe different stacks (refs #168)
ADR-0033 shipped the chart with this cost written down: "a second deployment description to keep in step with compose. Nothing enforces that today; a drift check belongs in CI (follow-up)." An upstream image bump or a new service applied to only one of the two files lands unnoticed. `make k8s-drift` compares what each stack actually deploys — workload names and resolved container images, from `docker compose config` and a rendered chart — rather than diffing the two files, which differ by design. Red: it reports the six differences that exist today, all of them the platform deviations ADR-0033 forced. Declaring those as intended is the green step.
This commit is contained in:
@@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help
|
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-drift k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help
|
||||||
|
|
||||||
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
||||||
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
||||||
@@ -350,6 +350,13 @@ k8s-lint:
|
|||||||
helm lint $(K8S_CHART)
|
helm lint $(K8S_CHART)
|
||||||
helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null
|
helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null
|
||||||
|
|
||||||
|
## k8s-drift: fail if compose and the Helm chart describe different stacks
|
||||||
|
# Compose is CI-canonical (ADR-0033) and the chart is a transcription of it; this
|
||||||
|
# compares what each one deploys — workload names and resolved images. Needs
|
||||||
|
# `docker compose` and `helm`, no cluster.
|
||||||
|
k8s-drift:
|
||||||
|
python3 infra/helm/check-drift.py
|
||||||
|
|
||||||
## k8s-registry: deploy the in-cluster image registry (NodePort 30500)
|
## k8s-registry: deploy the in-cluster image registry (NodePort 30500)
|
||||||
k8s-registry:
|
k8s-registry:
|
||||||
kubectl apply -f infra/helm/registry.yaml
|
kubectl apply -f infra/helm/registry.yaml
|
||||||
|
|||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail when the compose stack and the Helm chart stop describing the same stack.
|
||||||
|
|
||||||
|
`infra/docker-compose.yml` is CI-canonical; `infra/helm/big-reference` is a
|
||||||
|
transcription of it (ADR-0033), and until now nothing kept the two in step — an
|
||||||
|
upstream image bump or a new service applied to only one of them landed
|
||||||
|
unnoticed. This compares what each side actually *deploys*, not the two files:
|
||||||
|
the rendered chart against `docker compose config`. Both tools are already
|
||||||
|
prerequisites of the `k8s-*` make targets.
|
||||||
|
|
||||||
|
Run it with `make k8s-drift`. No cluster needed.
|
||||||
|
|
||||||
|
ponytail: names and images only, as sets — no per-workload env/ports/volumes.
|
||||||
|
Those differ by design in four documented places (ADR-0033), so comparing them
|
||||||
|
would mean re-encoding every deviation field by field; a tag bump and a missing
|
||||||
|
service are the drift that actually bites.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
COMPOSE = ROOT / "infra/docker-compose.yml"
|
||||||
|
CHART = ROOT / "infra/helm/big-reference"
|
||||||
|
|
||||||
|
# The busybox init container that every `waitFor` workload gets exists only in
|
||||||
|
# the chart (compose has `depends_on`). Rendering it under a sentinel makes it
|
||||||
|
# filterable without teaching the check what busybox is.
|
||||||
|
BUSYBOX = "drift-check-ignored-init-image"
|
||||||
|
|
||||||
|
# Workloads the observability backplane adds. Off by default in both stacks'
|
||||||
|
# defaults, so they are rendered on purpose here — otherwise their images drift
|
||||||
|
# unwatched.
|
||||||
|
OBSERVABILITY = ["tempo", "prometheus", "grafana"]
|
||||||
|
|
||||||
|
|
||||||
|
def compose_services() -> dict[str, str]:
|
||||||
|
"""Service name -> image, with ${TAG:-default} interpolation already applied."""
|
||||||
|
out = run(["docker", "compose", "-f", str(COMPOSE), "config", "--format", "json"])
|
||||||
|
return {name: svc.get("image", "") for name, svc in json.loads(out)["services"].items()}
|
||||||
|
|
||||||
|
|
||||||
|
def chart_workloads() -> dict[str, str]:
|
||||||
|
"""Workload name -> image, read back out of the rendered manifests."""
|
||||||
|
out = run(
|
||||||
|
["helm", "template", "big", str(CHART), "-n", "big", "--set", f"images.busybox={BUSYBOX}"]
|
||||||
|
+ [f"--set=workloads.{w}.enabled=true" for w in OBSERVABILITY]
|
||||||
|
)
|
||||||
|
workloads = {}
|
||||||
|
for doc in out.split("\n---"):
|
||||||
|
if not re.search(r"^kind: (Deployment|Job)$", doc, re.M):
|
||||||
|
continue
|
||||||
|
name = re.search(r"^ name: (\S+)$", doc, re.M)[1]
|
||||||
|
images = [i for i in re.findall(r"^\s+image: (\S+)$", doc, re.M) if i != BUSYBOX]
|
||||||
|
workloads[name] = images[0]
|
||||||
|
return workloads
|
||||||
|
|
||||||
|
|
||||||
|
def run(argv: list[str]) -> str:
|
||||||
|
proc = subprocess.run(argv, capture_output=True, text=True)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
sys.exit(f"{argv[0]} failed:\n{proc.stderr}")
|
||||||
|
return proc.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
compose, chart = compose_services(), chart_workloads()
|
||||||
|
problems = []
|
||||||
|
|
||||||
|
for name in sorted(set(compose) - set(chart)):
|
||||||
|
problems.append(f" {name}: in docker-compose.yml, not in the chart")
|
||||||
|
for name in sorted(set(chart) - set(compose)):
|
||||||
|
problems.append(f" {name}: in the chart, not in docker-compose.yml")
|
||||||
|
for name in sorted(set(compose) & set(chart)):
|
||||||
|
if compose[name] != chart[name]:
|
||||||
|
problems.append(f" {name}: compose runs {compose[name]}, the chart runs {chart[name]}")
|
||||||
|
|
||||||
|
if problems:
|
||||||
|
print("compose and the Helm chart describe different stacks:\n" + "\n".join(problems))
|
||||||
|
print(
|
||||||
|
"\nPort the change to the other stack, or — if the difference is forced by\n"
|
||||||
|
"Kubernetes — declare it in DEVIATIONS in this file, with the reason."
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"no drift: {len(chart)} workloads, images identical on both stacks")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user