diff --git a/ci/tests/touched-requirements.test.sh b/ci/tests/touched-requirements.test.sh index 5c0b028..e7ae926 100755 --- a/ci/tests/touched-requirements.test.sh +++ b/ci/tests/touched-requirements.test.sh @@ -136,6 +136,65 @@ printf 'touched set (clusters/ path map):\n%s\n---\n' "$OUT4" assert_present "R-ENV-02" "$OUT4" "clusters/* attributes R-ENV-02 (selects placeholder-guard-coupling HALF A)" assert_present "R-GITOPS-01" "$OUT4" "clusters/* attributes R-GITOPS-01" +# --------------------------------------------------------------------------------------- +# PATH MAP — apps/*, the C2 UI, and the platform assembly (added 2026-09-19) +# +# These arms closed the largest remaining fail-open hole in the map. Before them, 49 tracked +# app manifests (apps/wave-0, -1, -2, apps/vms) plus platform/homepage, platform/headlamp, +# platform/gateway, platform/kubevirt, platform/crafty, platform/coredns, the snapshot/backup +# machinery and platform/kustomization.yaml all attributed NOTHING — so a change to any of them +# selected only the whole-repo-scoped checks, and every requirement-scoped L2 behaviour test +# (default-deny, egress class, auth mode, C2 surface) was filtered out of the merge gate. +# +# The C2 case was already documented as a known gap in changed_diff()'s rule-3 comment: the ONLY +# route to R-C2-01/02 was rule 3, i.e. editing the C2 test itself. +# +# As with bootstrap/ above, each fixture cites NO R-ID of its own, so any R-ID in the output came +# from the PATH and not from the content scan. + +# apps/vms/* must win over apps/* — `case` takes the FIRST match, so arm ORDER is the property +# under test here, not just the presence of both arms. +git -C "$TMP" checkout -q -b feature-apps-vms main +mkdir -p "$TMP/apps/vms/demo" +printf 'kind: VirtualMachine\nmetadata:\n name: demo\n' > "$TMP/apps/vms/demo/vm.yaml" +git -C "$TMP" add -A && git -C "$TMP" commit -qm "touch apps/vms/" +OUT_VMS="$(REPO_ROOT="$TMP" BASE_REF=main "$SCRIPT" 2>/dev/null)" +printf 'touched set (apps/vms/ path map):\n%s\n---\n' "$OUT_VMS" +assert_present "R-PLAT-03" "$OUT_VMS" "apps/vms/* attributes R-PLAT-03 (KubeVirt VM lifecycle)" +assert_present "R-STATE-01" "$OUT_VMS" "apps/vms/* attributes R-STATE-01 (VM root/data volumes)" +assert_absent "R-PLAT-02" "$OUT_VMS" "apps/vms/* does NOT fall through to the apps/* container arm (order holds)" + +# A plain ServiceDefinition under apps/ gets the container-workload set. +git -C "$TMP" checkout -q -b feature-apps main +mkdir -p "$TMP/apps/wave-1" +printf 'kind: ServiceDefinition\nmetadata:\n name: demo\n' > "$TMP/apps/wave-1/demo.yaml" +git -C "$TMP" add -A && git -C "$TMP" commit -qm "touch apps/wave-1/" +OUT_APPS="$(REPO_ROOT="$TMP" BASE_REF=main "$SCRIPT" 2>/dev/null)" +printf 'touched set (apps/ path map):\n%s\n---\n' "$OUT_APPS" +assert_present "R-PLAT-02" "$OUT_APPS" "apps/* attributes R-PLAT-02 (container workload render)" +assert_present "R-NET-01" "$OUT_APPS" "apps/* attributes R-NET-01 (default-deny applies to every app)" +assert_present "R-NET-06" "$OUT_APPS" "apps/* attributes the egress-class requirements" +assert_present "R-IDP-02" "$OUT_APPS" "apps/* attributes the auth-mode requirements" + +# homepage / headlamp ARE the command-centre surface. +git -C "$TMP" checkout -q -b feature-c2 main +mkdir -p "$TMP/platform/headlamp" +printf 'kind: Deployment\nmetadata:\n name: headlamp\n' > "$TMP/platform/headlamp/deployment.yaml" +git -C "$TMP" add -A && git -C "$TMP" commit -qm "touch platform/headlamp/" +OUT_C2="$(REPO_ROOT="$TMP" BASE_REF=main "$SCRIPT" 2>/dev/null)" +printf 'touched set (C2 path map):\n%s\n---\n' "$OUT_C2" +assert_present "R-C2-01" "$OUT_C2" "platform/headlamp/* attributes R-C2-01 (was reachable only by editing the C2 test)" +assert_present "R-C2-02" "$OUT_C2" "platform/headlamp/* attributes R-C2-02" + +# platform/kustomization.yaml is the assembly: one deleted line disables a whole component. +git -C "$TMP" checkout -q -b feature-assembly main +mkdir -p "$TMP/platform" +printf 'resources:\n - cilium\n - longhorn\n' > "$TMP/platform/kustomization.yaml" +git -C "$TMP" add -A && git -C "$TMP" commit -qm "touch platform/kustomization.yaml" +OUT_ASM="$(REPO_ROOT="$TMP" BASE_REF=main "$SCRIPT" 2>/dev/null)" +printf 'touched set (platform assembly):\n%s\n---\n' "$OUT_ASM" +assert_present "R-GITOPS-01" "$OUT_ASM" "platform/kustomization.yaml attributes R-GITOPS-01 (component assembly)" + # --------------------------------------------------------------------------------------- # THE ID PATTERN MUST ALLOW A DIGIT IN THE WORKSTREAM SEGMENT (added 2026-09-13) # diff --git a/ci/touched-requirements.sh b/ci/touched-requirements.sh index 8566f40..6667644 100755 --- a/ci/touched-requirements.sh +++ b/ci/touched-requirements.sh @@ -177,6 +177,23 @@ path_requirements() { case "$1" in catalog/*) echo "R-GITOPS-02" ;; schemas/*) echo "R-GITOPS-02" ;; + # apps/* was UNMAPPED until 2026-09-19, and it is the largest such hole in the map: 49 tracked + # manifests (apps/wave-0, -1, -2, apps/vms) that describe every workload on the cluster. A change + # adding or editing an app attributed NOTHING, so the merge gate ran only the whole-repo-scoped + # checks and none of the requirement-scoped L2 behaviour tests — default-deny enforcement, egress + # class enforcement, auth-mode enforcement — which are exactly what a new or edited app can + # violate. Kyverno admission and the L0 egress-path check are real backstops, but "admission will + # catch it at apply time" is post-merge, and the gate exists to catch it before. + # + # ORDER MATTERS: `case` takes the FIRST match, so apps/vms/* must precede apps/*. + # apps/vms/ are KubeVirt VMs (R-PLAT-03 lifecycle) with root/data volumes (R-STATE-01), still + # subject to default-deny (R-NET-01). + apps/vms/*) echo "R-PLAT-03 R-NET-01 R-STATE-01" ;; + # Everything else under apps/ is a ServiceDefinition rendered by catalog/render.py: a container + # workload (R-PLAT-02) rendered from the catalog (R-GITOPS-02), carrying the egress-class + # (R-NET-05/06/07) and auth-mode (R-IDP-02/R-IDP-06) labels that admission requires, under + # default-deny (R-NET-01). + apps/*) echo "R-PLAT-02 R-GITOPS-02 R-NET-01 R-NET-05 R-NET-06 R-NET-07 R-IDP-02 R-IDP-06" ;; platform/kyverno/*) echo "R-TEST-01 R-NET-01 R-SEC-01" ;; platform/cilium/*) echo "R-NET-01 R-NET-02 R-NET-05" ;; platform/longhorn/*) echo "R-STATE-01 R-STATE-02" ;; @@ -208,6 +225,39 @@ path_requirements() { platform/keycloak/*) echo "R-IDP-01 R-IDP-02 R-IDP-03 R-IDP-04 R-IDP-05 R-IDP-06" ;; platform/oauth2-proxy/*) echo "R-IDP-02 R-IDP-06" ;; platform/image-automation/*) echo "R-UPD-01 R-UPD-02" ;; + # The C2 UI was UNMAPPED until 2026-09-19. This is the gap named explicitly in changed_diff()'s + # rule-3 comment above: "path_requirements() has no arm for platform/homepage/* or + # platform/headlamp/* either, so the ONLY route to R-C2-01/02 was rule 3 (editing the C2 test + # itself)". homepage + headlamp ARE the command-centre surface (registry: R-C2-01/02 -> + # docs/12-command-center-ui.md), so editing them must select the C2 tests. + platform/homepage/*|platform/headlamp/*) echo "R-C2-01 R-C2-02" ;; + # KubeVirt is the VM platform itself (R-PLAT-03); its virt-* components are also scrape targets. + platform/kubevirt/*) echo "R-PLAT-03 R-OBS-01" ;; + # The shared ingress Gateway: the L2-announced VIP every HTTP service answers on (R-DNS-01), the + # TLS listeners and their certificates (R-TLS-01/02), under default-deny (R-NET-01). + platform/gateway/*) echo "R-DNS-01 R-TLS-01 R-TLS-02 R-NET-01" ;; + # CoreDNS is cluster DNS resolution (R-DNS-01). + platform/coredns/*) echo "R-DNS-01" ;; + # crafty is a first-class app living under platform/: a container workload with a Longhorn PVC and + # its OWN L4 LoadBalancer VIP (Minecraft 25565 is not HTTP, so it does not use the Gateway). + platform/crafty/*) echo "R-PLAT-02 R-NET-01 R-DNS-01 R-STATE-01" ;; + # Snapshot machinery is the storage/DR path: CSI snapshots (R-STATE-01) feeding backup (R-STATE-03). + platform/external-snapshotter/*|platform/backup/*) echo "R-STATE-01 R-STATE-03" ;; + # runbooks/ IS the R-OBS-04 surface: alert-runbooks-resolvable asserts every alert's runbook_url + # resolves to a served page, and runbooks/serving/ is the Deployment that serves them. That check + # is whole-repo-scoped so it runs regardless, but the attribution should still be honest — a + # runbook change is an observability change. + runbooks/*) echo "R-OBS-04" ;; + # platform-config/ is the env-specific configuration Flux substitutes into and applies. + platform-config/*|platform-config-cdi/*) echo "R-ENV-02 R-GITOPS-01" ;; + # platform/kustomization.yaml is the ASSEMBLY: the list of platform components Flux applies. + # Deleting one line there disables an entire component (cilium, longhorn, velero...) while every + # file it points at stays perfectly valid on disk, so nothing else in the map would attribute the + # change. That is the most consequential single-line edit in the repo and it attributed nothing. + platform/kustomization.yaml) echo "R-GITOPS-01" ;; + # .sops.yaml holds the encryption rules (which paths must be encrypted, with which keys). A change + # here can silently stop a new secret path from being encrypted, which is R-SEC-01's whole point. + .sops.yaml) echo "R-SEC-01 R-GITOPS-01" ;; conformance/*) echo "R-TEST-01 R-TEST-02 R-TEST-03 R-TEST-04" ;; # infra/ was UNMAPPED until 2026-09-12 — the OpenTofu + Ansible that provision the dev # nodes. conformance/tests/l0-static/dev-nodes-provisioned-as-code reads NOTHING but