Three layers of validation run before changes reach main:
- Pre-commit hook + render checks — fast, local.
- Argo CD overlay smoke test (
kind-argo-smoke.yml) — RBAC/PDB/netpol assertions against the HA Argo CD install on an ephemeral kind cluster. - Component e2e (
e2e.yml) — on PRs that bump a cluster component (typically Renovate), the changed component is deployed on kind through Argo CD at the PR's revision and its services are exercised.
.githooks/pre-commit is the first line of defense.
Install it once per clone:
git config core.hooksPath .githooksIt enforces:
- static AppProject RBAC denylist checks (no static project may forget to
blacklist
RoleandRoleBinding, unless explicitly opted out with# rbac-lint: ignore) - feature-branch revision checks for critical manifests (
base/argo-projects.yaml,base/mip-infrastructure/mip-infrastructure.yaml,common/security/netpol.yaml)
Before opening a PR, make sure the key manifests still render cleanly:
kubectl kustomize argo-setup/patches >/dev/null
kubectl apply --dry-run=client -f base/argo-projects.yaml >/dev/null
kubectl apply --dry-run=client -f projects/mip-infrastructure.yaml >/dev/null
kustomize build common/notebook-operator/manifests >/dev/null
helm template netpol common/security/federation --set targetNamespace=federation-a >/dev/null
helm template netpol common/security/common-templates --set targetNamespace=mip-athena >/dev/nullThe e2e render job also checks the nginx configuration of the notebook API
proxy with nginx -t inside the pinned image.
If you changed a specific component under common/ or deployments/, render
that path too.
scripts/kind-argo-test.sh spins up a kind
cluster (1 control-plane + 3 workers — the redis-ha anti-affinity needs 3
schedulable nodes), applies argo-setup/patches, and asserts:
- all HA workloads reach Ready
- the tightened ClusterRoles are effective (positive + negative SubjectAccessReviews)
- PDBs exist and select live pods
- static AppProjects apply;
defaultstays deny-all - the repo-server NetworkPolicy actually blocks unauthorized traffic
- the notebook RBAC reconciler binds only in labelled
federation-*namespaces, copies the proxy CA there (from a CertificateRequest status, with the cert-manager CRDs installed for the test), prunes elsewhere, and the admission policies reject every other RoleBinding and ConfigMap from its account and every other Secret from the hub (server-side dry runs as those accounts)
CI runs it on PRs touching argo-setup/**, the static projects and the
notebook RBAC files
(kind-argo-smoke.yml). Locally:
bash scripts/kind-argo-test.sh # spin up, test, tear down
KEEP=1 bash scripts/kind-argo-test.sh # leave the cluster running.github/workflows/e2e.yml maps the files a
PR changes to profiles (mapping lives in
scripts/e2e/changed-profiles.sh) and
runs scripts/e2e/run-e2e.sh for each requested
profile on its own runner. Every profile installs the full HA Argo CD overlay
first, then deploys the component exactly like production — as an Argo CD
Application — and asserts service-level health: Argo Synced/Healthy,
rollout status, and in-cluster curl against the Services asserting response
content (an ingress 200 alone proves nothing about the app behind it).
| Profile | Triggered by | Deploys | Key assertions |
|---|---|---|---|
exareme2 |
deployments/shared-apps/exareme2/**, federation exareme2 overrides |
federation-A exareme2 | app Healthy; controller/worker/aggregation rollouts; PVCs Bound; /healthcheck (controller verifies its workers); /algorithms non-empty |
mip-stack |
deployments/shared-apps/mip-stack/**, federation mip-stack overrides |
exareme2 (merged pin) + mip-stack | everything above; backend + UI rollouts; pg_isready; UI serves HTML; backend answers on a known endpoint |
eck |
common/monitoring/** |
ECK operator (at the PR's operator.version) + 1-node ES + Kibana |
Elasticsearch/Kibana CRs report green; ES /_cluster/health green via API; Kibana /api/status available |
haproxy |
common/haproxy-ingress/**, its out-of-band RBAC |
haproxy-public + MetalLB + cert-manager | app Healthy; Service holds the pinned VIP; default cert Ready; echo-backend body served through the Ingress; unknown Host refused |
datacatalog |
common/datacatalog/** |
datacatalog | app Healthy; rollouts; PVCs Bound; backend actuator UP; frontend serves HTML |
render |
common/submariner/**, federation-Z files |
nothing (build-only) | submariner charts + federation overlays kustomize build cleanly |
Submariner gets no live profile: a single kind cluster cannot form an IPsec mesh, so only the chart render is checked.
Committed Application manifests must keep targetRevision: main (enforced by
check-main-revisions.yml). The e2e runner renders the manifests and rewrites
targetRevision: main → the PR head SHA at runtime, then asserts that no
main reference and no mip-deployments reference survived (the check_zero
pattern). Argo CD inside kind then fetches that commit from the public repo.
Consequences:
- the branch must be pushed before the e2e can run (also true locally)
- fork PRs are skipped — Argo cannot fetch fork commits from the upstream repo URL. Renovate PRs are same-repo, so they are always tested.
- the e2e tests the PR head, not the would-be merge result
The production cluster contract is faked with the smallest possible shims; production manifests and values deploy unmodified except where kind cannot comply:
| Production expectation | CI shim |
|---|---|
StorageClasses ceph-corbo-cephfs, ceph-corbo-cephfs-retain (CephFS, RWX) |
same names backed by rancher.io/local-path; RWX claims downgraded to RWO via CI values |
MetalLB pool pool-no-auto with VIP 148.187.143.44 |
MetalLB installed with a /32 pool containing exactly that VIP (haproxy profile) |
| cert-manager + letsencrypt ClusterIssuers | cert-manager installed; letsencrypt issuers apply but ACME readiness is never load-bearing (a CI-only argocd-cm health override keeps Ready=False ClusterIssuers from degrading the app) |
| Keycloak at iam.ebrains.eu | keycloak.enabled: false + dummy keycloak-credentials (authentication off) |
| public ingress hosts | ingress disabled in federation/datacatalog profiles; exercised by the haproxy profile instead |
interactive scripts/gen_secrets.sh |
non-interactive scripts/e2e/ci-secrets.sh |
git push origin HEAD # Argo must be able to fetch the commit
PROFILE=exareme2 KEEP=1 bash scripts/e2e/run-e2e.sh
kind delete cluster --name mip-e2e # when doneHEAD_SHA=<sha> overrides the synced revision; CLUSTER=<name> the kind
cluster name. In CI each profile can also be run on demand via
workflow_dispatch (profile input).
Once the manifests are applied to a real cluster, verify the bootstrap objects and Applications show up as expected:
kubectl get applicationsets -n argocd-mip-team
kubectl get appprojects -n argocd-mip-team
argocd app list
argocd app get <app-name>Expected steady state:
- the
defaultAppProject is deny-all - the static AppProjects exist in
argocd-mip-team - the
mip-infrastructureApplicationSet creates the expected Applications - synced Applications move to
Healthyafter required secrets are present
scripts/gen_secrets.sh creates the per-federation
keycloak-credentials and mip-secret objects (and the common datacatalog
secret) interactively. Use it after the federation namespaces exist. CI uses
the non-interactive scripts/e2e/ci-secrets.sh
instead.