Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6aaed7c8c | ||
|
|
7a5840149c | ||
|
|
916d671d49 | ||
|
|
2d40c84e2c | ||
|
|
4edcf00267 | ||
|
|
51d99855d1 |
@@ -41,27 +41,6 @@ jobs:
|
|||||||
nuget-${{ runner.os }}-
|
nuget-${{ runner.os }}-
|
||||||
- run: make lint
|
- run: make lint
|
||||||
|
|
||||||
# The Helm chart's only automated gate: it renders and schema-checks the whole
|
|
||||||
# stack, and checks it still describes the same stack as the compose file
|
|
||||||
# (ADR-0033). No cluster involved — see docs/runbooks/kubernetes-talos.md.
|
|
||||||
k8s:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: https://github.com/actions/checkout@v4
|
|
||||||
# helm as its pinned static binary rather than a marketplace action: one URL,
|
|
||||||
# the same one the Talos runbook §0 gives a developer, and no third-party
|
|
||||||
# action to vet (CLAUDE.md §13). The drift check also needs `docker compose`,
|
|
||||||
# which the runner already has (see docs/runbooks/ci.md).
|
|
||||||
- name: Install helm
|
|
||||||
run: |
|
|
||||||
mkdir -p "$HOME/.local/bin"
|
|
||||||
curl -sSL https://get.helm.sh/helm-v3.16.4-linux-amd64.tar.gz \
|
|
||||||
| tar xz -O linux-amd64/helm > "$HOME/.local/bin/helm"
|
|
||||||
chmod +x "$HOME/.local/bin/helm"
|
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
||||||
- run: make k8s-lint
|
|
||||||
- run: make k8s-drift
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -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-drift 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-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,13 +350,6 @@ 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
|
||||||
|
|||||||
@@ -126,9 +126,8 @@ Consequences of that shape, each chosen deliberately:
|
|||||||
|
|
||||||
**Negative / costs**
|
**Negative / costs**
|
||||||
|
|
||||||
- A second deployment description to keep in step with compose. `make k8s-drift` (#168)
|
- A second deployment description to keep in step with compose. Nothing enforces that
|
||||||
now enforces the part that bites — the workload set and the resolved images, with the
|
today; a drift check belongs in CI (follow-up).
|
||||||
four deviations below declared — but not per-workload env, ports or volumes.
|
|
||||||
- `helm install` alone is not enough — the ConfigMaps must be seeded first, and a missing
|
- `helm install` alone is not enough — the ConfigMaps must be seeded first, and a missing
|
||||||
one surfaces as `ContainerCreating`, not as a clear error.
|
one surfaces as `ContainerCreating`, not as a clear error.
|
||||||
- Generic templates mean a values typo can render valid-but-wrong YAML; `k8s-lint` catches
|
- Generic templates mean a values typo can render valid-but-wrong YAML; `k8s-lint` catches
|
||||||
|
|||||||
+2
-6
@@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
> **Status: active.** The workflow `.gitea/workflows/ci.yaml` runs on Gitea's
|
> **Status: active.** The workflow `.gitea/workflows/ci.yaml` runs on Gitea's
|
||||||
> hosted `ubuntu-latest` runner — no self-hosted runner required.
|
> hosted `ubuntu-latest` runner — no self-hosted runner required.
|
||||||
> **`make ci` is still the local gate** — it runs the same checks via the same
|
> **`make ci` is still the local gate** — it runs the exact same checks
|
||||||
> `make` targets, with one exception: the `k8s` job's targets are not in `make ci`,
|
> (the workflow calls the same `make` targets).
|
||||||
> because `helm` is optional for everyone not deploying to Kubernetes. Run
|
|
||||||
> `make k8s-lint k8s-drift` by hand after touching the chart or the compose file.
|
|
||||||
|
|
||||||
## The pipeline
|
## The pipeline
|
||||||
|
|
||||||
@@ -18,8 +16,6 @@ and CI cannot drift:
|
|||||||
| `lint` | `make lint` → `dotnet format … --verify-no-changes` | .NET 10 SDK |
|
| `lint` | `make lint` → `dotnet format … --verify-no-changes` | .NET 10 SDK |
|
||||||
| `build` | `make build` → `dotnet build … -c Release` | .NET 10 SDK |
|
| `build` | `make build` → `dotnet build … -c Release` | .NET 10 SDK |
|
||||||
| `unit` | `make unit` → `dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK |
|
| `unit` | `make unit` → `dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK |
|
||||||
| `frontend` | `make frontend` → Nx lint/test/build for the four portals | pnpm + Node |
|
|
||||||
| `k8s` | `make k8s-lint` (render + schema-check the Helm chart) → `make k8s-drift` (chart still describes the same stack as `infra/docker-compose.yml`) | pinned `helm` binary + `docker compose` |
|
|
||||||
| `mutation` | `make mutation` → `dotnet tool restore` → `dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
|
| `mutation` | `make mutation` → `dotnet tool restore` → `dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
|
||||||
| `verify-stack` | the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
|
| `verify-stack` | the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
|
||||||
|
|
||||||
|
|||||||
@@ -322,7 +322,6 @@ The PVCs carry `helm.sh/resource-policy: keep`, so `make k8s-down` leaves the da
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
make k8s-lint # render + schema-check the chart, no cluster needed
|
make k8s-lint # render + schema-check the chart, no cluster needed
|
||||||
make k8s-drift # fail if compose and the chart describe different stacks
|
|
||||||
make k8s-portals # forward the portals + Keycloak to localhost (browser access)
|
make k8s-portals # forward the portals + Keycloak to localhost (browser access)
|
||||||
make k8s-images K8S_REGISTRY=... # after changing a service or a portal
|
make k8s-images K8S_REGISTRY=... # after changing a service or a portal
|
||||||
make k8s-up TALOS_HOST=... K8S_REGISTRY=...
|
make k8s-up TALOS_HOST=... K8S_REGISTRY=...
|
||||||
@@ -367,7 +366,5 @@ immutable, so `helm upgrade` is rejected with `cannot patch "…" with kind Job`
|
|||||||
`K8S_SET='--set workloads.tempo.enabled=true --set workloads.prometheus.enabled=true --set workloads.grafana.enabled=true'`.
|
`K8S_SET='--set workloads.tempo.enabled=true --set workloads.prometheus.enabled=true --set workloads.grafana.enabled=true'`.
|
||||||
The .NET services still export OTLP; the exporter fails harmlessly when Tempo is absent.
|
The .NET services still export OTLP; the exporter fails harmlessly when Tempo is absent.
|
||||||
- **The verify/e2e lanes.** `make verify*` and the Playwright e2e drive compose, not the
|
- **The verify/e2e lanes.** `make verify*` and the Playwright e2e drive compose, not the
|
||||||
chart. The Kubernetes path is verified with §5's smoke test. CI's `k8s` job runs the two
|
chart. The Kubernetes path is verified with §5's smoke test.
|
||||||
clusterless checks (`k8s-lint`, `k8s-drift`) on every PR — a values typo or a compose
|
|
||||||
image bump that skipped the chart fails there, but nothing deploys the chart in CI.
|
|
||||||
- **Ingress, TLS, and resource requests.** See the ponytail ceiling in ADR-0033.
|
- **Ingress, TLS, and resource requests.** See the ponytail ceiling in ADR-0033.
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
|
|
||||||
# Differences that Kubernetes forces, not drift (ADR-0033). A name listed here is
|
|
||||||
# expected to be on exactly one side; anything else fails.
|
|
||||||
DEVIATIONS = {
|
|
||||||
# The four Django services apply their own setup_configuration in the web pod
|
|
||||||
# (`args: [sh, -c, "/setup_configuration.sh && exec /start.sh"]`) rather than in a
|
|
||||||
# separate init Job. Both that script and /start.sh run `manage.py migrate`, and
|
|
||||||
# Kubernetes has no `depends_on: service_completed_successfully` to serialise them,
|
|
||||||
# so the Job and its web pod migrated the same database concurrently.
|
|
||||||
"oz-init": "folded into the openzaak pod",
|
|
||||||
"nrc-init": "folded into the nrc-web pod",
|
|
||||||
"objecttypen-init": "folded into the objecttypen pod",
|
|
||||||
"objecten-init": "folded into the objecten pod",
|
|
||||||
# Compose seeds these from the host — the verify scripts `docker cp` the two
|
|
||||||
# scripts into a running container, and docker-compose.local.yml carries
|
|
||||||
# `local-seed` + `nrc-subscribe` for `make local`. A cluster has no host to seed
|
|
||||||
# from, so both became Jobs in the chart.
|
|
||||||
"seed-zaaktype": "compose seeds the catalogus from the host (infra/openzaak/seed_catalogus.py)",
|
|
||||||
"nrc-subscribe": "compose registers the abonnement from the host (infra/local/register-abonnement.py)",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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) - set(DEVIATIONS)):
|
|
||||||
problems.append(f" {name}: in docker-compose.yml, not in the chart")
|
|
||||||
for name in sorted(set(chart) - set(compose) - set(DEVIATIONS)):
|
|
||||||
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")
|
|
||||||
for name, why in sorted(DEVIATIONS.items()):
|
|
||||||
print(f" deviation (declared): {name} — {why}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
Reference in New Issue
Block a user