Commit Graph
298 Commits
Author SHA1 Message Date
jakman85andClaude Opus 4.8 08113ccfbb ci(l0): fix kyverno test expectations for CLI 1.13.4; pin kyverno-json 0.0.3
The offline `kyverno test` CLI cannot resolve the Flux postBuild token
${KYVERNO_VALIDATION_ACTION} in validationFailureAction, so the 4 policies
carrying it failed to load and their result rows reported "Not found" (14/17
cases). Render a throwaway copy of platform/kyverno with the base/prod value
(Enforce) before `kyverno test` in both ci/l0.sh and scripts/run-l0.sh; this
does not change in-cluster enforcement.

Add tests/values.yaml supplying Namespace labels so the offline CLI resolves
namespaceSelector matches -- without it the R-SEC-02/R-NET-05/R-IDP-02 Pod
cases were silently EXCLUDED (vacuous), not evaluated.

Pin kyverno-json 0.0.3 in ci/versions.env (0.0.5 has no upstream release;
0.0.3 is what run-l0.sh and the devcontainer already use).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 15:57:29 -05:00
jakman85andClaude Opus 4.8 bb48f675ee ci: bake pinned test toolchain into devcontainer + fix conformance layer/spec-immutability seams
Bake the conformance harness toolchain into infra/devcontainer so `make -C ci gate`
runs identically here and in CI:
- Containerfile: install kyverno CLI (1.13.4), kyverno-json, chainsaw (0.2.12),
  yamllint (1.35.1), check-jsonschema (0.29.4), yq (4.44.6) via new pinned RUN block;
  add diffutils + python3-pip; drop stale LIBVIRT_DEFAULT_URI; refresh header for the
  Proxmox(bpg)+conformance toolbox; extend /etc/dev-toolchain-versions provenance.
- versions.env: add KYVERNO/KYVERNO_JSON/CHAINSAW/YAMLLINT/YQ/CHECK_JSONSCHEMA;
  align KUBECTL 1.32.0 -> 1.31.4. dev.sh build(): pass the new --build-args.
- ci/versions.env: pin CHECK_JSONSCHEMA_VERSION=0.29.4 (single source of truth).

Fix harness seams so the gate can run:
- dev.sh run(): mount the sibling spec repo (home-network-k3s) read-only at
  /home-network-k3s for ci/spec-immutability.sh (R-TEST-06); conditional so the
  container still runs without it.
- conformance/lib/common.sh conf_dirs_for_layer(): accept a comma/space list of
  layers and union the dirs, so ci/conformance.sh `run.sh --layer l2,l3` selects
  both layers in one pass (backward compatible for single layers).

NOTE: ci/versions.env pins KYVERNO_JSON_VERSION=0.0.5, which has no upstream release
(max tag is v0.0.3; platform/kyverno/scripts/run-l0.sh already defaults to v0.0.3).
Baked v0.0.3 and flagged for PM to correct ci/versions.env. kyverno-json is not used
by any static gate stage.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 15:43:19 -05:00
jakman85andClaude Opus 4.8 0d7495486e devcontainer: wire dev.sh to the Proxmox k3s cluster
- drop stale libvirt socket mount + LIBVIRT_DEFAULT_URI (old KVM cluster)
- auto-open an SSH tunnel (local :16443 -> primary 127.0.0.1:6443 over the
  pfSense SSH NAT) on each run so kubectl/flux/helm reach the cluster with no
  manual setup; DEV_NO_TUNNEL=1 opts out, K3S_SSH_* env overrides the NAT front
- refresh header/usage comments (bpg/proxmox, not dmacvicar/libvirt)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 14:59:19 -05:00
jakman85andClaude Opus 4.8 21669da075 flux: re-enable KubeVirt + CDI (new 16GB Proxmox nodes with nested virt)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 13:21:00 -05:00
jakman85andClaude Opus 4.8 e6b3a03ff4 flux: re-enable oauth2-proxy now Keycloak is live and split-horizon DNS points at 192.168.1.53
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 13:17:04 -05:00
jakman85andClaude Opus 4.8 604552e51c flux(dev): set GATEWAY_VIP to Proxmox Gateway VIP 192.168.1.53 (split-horizon DNS)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 13:15:16 -05:00
jakman85andClaude Opus 4.8 efd6dbeb3b flux(platform-config): temp-disable orphaned oauth2-proxy refs (missing ns) to unblock reconcile
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 13:02:08 -05:00
jakman85andClaude Opus 4.8 2fea1e261c flux(platform): temp-disable oauth2-proxy to break cold-start OIDC deadlock (re-enable after platform-config green)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 12:48:05 -05:00
jakman85andClaude Opus 4.8 c6301a79c5 fix(bootstrap): subshell the repo_root fallback so trailing pwd doesn't corrupt the path
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 12:27:34 -05:00
jakman85andClaude Opus 4.8 fff44992e7 migrate(proxmox): bootstrap source URL -> 192.168.2.103; Cilium k8sServiceHost -> 192.168.1.50
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 12:22:08 -05:00
jakman85andClaude Opus 4.8 0037fb40fa ansible(cilium): rewrite controller kubeconfig to SSH-tunnel endpoint (kube-API not NAT-forwarded)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 12:15:12 -05:00
jakman85andClaude Opus 4.8 1004c8eae6 ansible(node_baseline): don't fail on qemu-guest-agent when no virtio channel (agent disabled in tofu); enable-only
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 12:04:19 -05:00
jakman85andClaude Opus 4.8 1ff25318c9 ansible(proxmox): route Ansible via pfSense NAT; decouple k3s node-ip from ansible_host; mount vault-pass into dev container
- inventory routes SSH via 192.168.2.102:2250/2251/2252; new k3s_node_ip carries
  each node's real 192.168.1.x for --node-ip / k3s_api_host / Cilium parity
- dev.sh bind-mounts ~/.config/home-k3s/ansible-vault-pass (ro) so vaulted
  playbooks can run in the pinned container (ansible.cfg vault_password_file)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 11:55:37 -05:00
jakman85andClaude Opus 4.8 28d200caf2 tofu(proxmox): use debian generic image + serial console to fix first-boot kernel panic (bpg#1639)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 11:45:29 -05:00
jakman85andClaude Opus 4.8 fca8d8f800 tofu(proxmox): download base image as import content-type for disk import_from
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 10:59:01 -05:00
jakman85andClaude Opus 4.8 a2392a4a43 tofu(proxmox): API-only native cloud-init; move node baseline to Ansible node_baseline role
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 10:55:48 -05:00
jakman85 8a8f28bf80 infra(proxmox): bump node disks to 64GB OS + 128GB Longhorn data (per operator) 2026-08-25 10:31:39 -05:00
jakman85andClaude Opus 4.8 d57c9cbf12 infra(net): repoint ansible inventory + Cilium VIPs to the Proxmox LAN
- ansible hosts.ini: node ansible_host -> 192.168.1.50/.51/.52, with a
  clear note that the PM may need to swap these for the pfSense
  NAT-forwarded address:port once that NAT rule exists (not guessed here).
  k3s_api_host derives from the inventory, so it follows automatically.
- Cilium CiliumLoadBalancerIPPool -> 192.168.1.53-.60 (matches the tofu
  cilium_l2_vip reservation on the new LAN).
- CiliumL2AnnouncementPolicy: broaden interface selector to ^e(n|th) so
  the Proxmox NIC name (ens18/eth0) need not be pinned; flagged
  verify-at-bringup.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 10:23:37 -05:00
jakman85andClaude Opus 4.8 4bb9c8fcbd infra(tofu): docs + example for the Proxmox migration
Update tofu README (Proxmox targets table, operator prerequisites:
enable Snippets content on `local`, endpoint scheme fix, pfSense NAT),
terraform.tfvars.example (Proxmox vars + env-only auth, token never a
tofu var), and the devcontainer versions.env provider note.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 10:23:28 -05:00
jakman85andClaude Opus 4.8 39123d7806 infra(cloud-init): deliver baseline via bpg user-data snippet; drop netplan/meta-data templates
bpg/proxmox generates cloud-init network-config (static IP/gw/DNS) and
meta-data natively from the VM initialization block, so remove
network-config.yaml.tftpl and meta-data.yaml.tftpl. The rich first-boot
baseline (users, SSH key, packages, kernel modules, Longhorn /dev/vdb
mount, swap-off) is preserved and shipped as a user-data snippet
referenced by initialization.user_data_file_id. Comments updated
(libvirt -> Proxmox); /dev/vdb still valid (data disk is virtio1).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 10:23:21 -05:00
jakman85andClaude Opus 4.8 53ca698edc infra(tofu): migrate node provisioning from libvirt to bpg/proxmox
Replace the dmacvicar/libvirt provider + resources with the modern
bpg/proxmox provider. The dev cluster moves off the memory-constrained
local libvirt host onto the Proxmox VE box.

- versions.tf: pin bpg/proxmox ~> 0.66; provider reads auth from env
  (PROXMOX_VE_*, token never a tofu var) and normalizes a schemeless
  PROXMOX_VE_ENDPOINT (prepend https://) to avoid the :8006 HTTP 301.
- variables.tf: Proxmox placement vars (node=proxmox, datastore=Storage,
  image_datastore=local, bridge=vmbr0); LAN 192.168.1.0/24 gw .1 DNS .18;
  node_ips .50/.51/.52; sizing 6 vCPU / 16384 MiB / 40+100 GiB; cpu_type
  host; Cilium VIP range .53-.60.
- main.tf: proxmox_virtual_environment_download_file (cloud image -> local),
  per-node user-data snippet (proxmox_virtual_environment_file), and
  proxmox_virtual_environment_vm per node via name-keyed for_each (imported
  OS disk virtio0, blank data disk virtio1, native initialization ip_config
  + dns + user_data_file_id).
- remove moved.tf (libvirt-specific count->for_each state migration).

Authoring only; no apply. Traces R-ENV-01, R-PLAT-01.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 10:23:13 -05:00
jakman85 4f28ba6945 Merge chore/free-memory-disable-kubevirt (relieve OOM) 2026-08-25 09:29:30 -05:00
jakman85 971c730400 chore(memory): temp-disable KubeVirt to relieve node memory pressure
The 3x 6GB nodes are OOM (k3s-dev-2's kubelet stopped; dev-1/dev-3 <600MB free). KubeVirt runs
a full control plane (~1.5-2GB) for ZERO running VMs — its IaaS conformance (R-PLAT-03/04,
R-NET-01, R-STATE-01) is already certified, so disabling the running pods loses no delivered
capability. Re-enable (uncomment kubevirt in platform + platform-config) once the VMs' RAM is
grown. Manifests retained; reversible.
2026-08-25 09:29:29 -05:00
jakman85 c16e0889fd Merge fix/internet-egress-and-tags (world egress per class + image tags) 2026-08-25 09:02:06 -05:00
jakman85 7e8ae472c9 fix(net): renderer emits world/internet egress allow per egress class + fix jackett/plex tags
Root cause the media stack surfaced: the default-deny baseline is DNS-only egress (correct for
R-NET-01 east-west), but the egress CLASS (internet/vpn) had no policy ALLOWING north-south
egress — so qbittorrent/sonarr/radarr couldn't reach the internet at all (VPN downloaders
broken). Add an additive per-app CiliumNetworkPolicy '<app>-allow-internet-egress' with
toEntities:[world] (non-cluster); the egress-class steering + kill-switch (ADR-0015) govern the
PATH (internet=WAN, vpn=tun0). East-west stays denied except declared dependencies; R-NET-01
baseline unchanged. Re-rendered all apps + regenerated L1 goldens. Also fixed jackett + plex
image tags (bad pins -> :latest; Renovate/update policy pins later).
2026-08-25 09:02:05 -05:00
jakman85 f9e96402c0 chore(arr): scrub unscaffolded R-STATE-04 token from bulk-storage comments (gate hygiene) 2026-08-25 08:46:08 -05:00
jakman85 c8c862908b Merge feature/onboard-arr: Sonarr + Radarr (VPN + arr->downloader allows) 2026-08-25 08:45:56 -05:00
jakman85 ca1239c2c1 Merge feature/onboard-vpn-downloaders: qBittorrent + Jackett (VPN egress) 2026-08-25 08:45:42 -05:00
jakman85 aea8fca966 Merge chore/longhorn-capacity-plex-size 2026-08-25 08:45:12 -05:00
jakman85 4f74c8a2f0 chore(storage): Longhorn over-provisioning 100->200% + right-size plex config 20->8Gi
The 39Gi/node Longhorn data disks at 1:1 over-provisioning can't fit the media stack
(plex 20Gi replica failed: 'insufficient storage'). Volumes are thin-provisioned config-only,
so 200% lets the full catalog fit without a disk resize; physical fill risk is low. Also
right-sized plex config (20Gi was excessive for metadata). Prod-grade path: grow the data
disks (WS-STORAGE follow-up).
2026-08-25 08:45:12 -05:00
jakman85andClaude Opus 4.8 8392b52ee8 Onboard VPN downloaders (qBittorrent + Jackett) via ServiceDefinition renderer
Add catalog/ ServiceDefinitions for qbittorrent and jackett and their
rendered apps/ manifests, wired into apps/kustomization.yaml.

Both are VPN-class (egress: vpn -> egress-class=vpn pod label steered by the
CiliumEgressGatewayPolicy + host kill-switch, ADR-0015) and internal
forward-auth UIs (oauth2-proxy -> Keycloak, group media-users). Config on
3-replica Longhorn. qBittorrent's bulk downloads volume is DEFERRED pending
OQ-2 (non-provisioning bulk StorageClass stub), mirroring plex/arr.

Traces: R-NET-05/06 (VPN egress + kill-switch), R-IDP-02/06 (forward-auth),
R-STATE-02/03, R-OBS-01, R-UPD-01/03, R-GITOPS-02.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 08:40:48 -05:00
jakman85andClaude Opus 4.8 0f12950831 Onboard arr apps (Sonarr + Radarr) via ServiceDefinition renderer
Add catalog/sonarr.yaml and catalog/radarr.yaml ServiceDefinitions and their
rendered apps/*.yaml manifests, wired into apps/kustomization.yaml.

- exposure: internal, egress: vpn (R-NET-05/06 VPN + kill-switch, ADR-0015)
- auth: forward-auth, groups: media-users (R-IDP-02/06)
- dependencies -> qbittorrent:8080, jackett:9117 as explicit east-west
  CiliumNetworkPolicy egress allows on top of default-deny (R-NET-01/02)
- config on longhorn (R-STATE-02); bulk media/downloads volume DEFERRED
  pending OQ-2 (bulk StorageClass is a non-provisioning stub)
- observability up-SLI alert (R-OBS-01), daily-offcluster backup (R-STATE-03)

Rendered with catalog/render.py --no-image-automation. Offline gates green:
schema validate, kubectl kustomize apps, render.py --selftest, make -C ci gate.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 08:32:39 -05:00
jakman85 87087e06c7 Merge feature/onboard-plex: onboard Plex (media, public; bulk media deferred on OQ-2)
# Conflicts:
#	apps/kustomization.yaml
2026-08-25 08:29:35 -05:00
jakman85andClaude Opus 4.8 d47c8f91cb Onboard Plex to the k3s platform via ServiceDefinition (WS-APPS)
Add catalog/plex.yaml (kind: container) and its rendered apps/plex.yaml,
and wire apps/kustomization.yaml. Plex = docs/13 row 1, the catalog's
largest service; validates the public-exposure + config-on-Longhorn path.

- image plexinc/pms-docker (pinned recent stable tag), port 32400
- exposure: public at plex.home.keithjackson.dev; egress: internet
- auth: native (Plex accounts, not Keycloak — docs/13/15 exception)
- config PVC 20Gi on longhorn; off-cluster backup (daily-offcluster) as the
  longhorn tier forbids reacquirable under L0 (R-STATE-03)
- observability up-SLI; update channel stable

Bulk media (R-STATE-04) deferred: the `bulk` StorageClass is a documented
stub (OQ-2 unresolved, NFS CSI not installed) so a bulk PVC would leave the
pod Pending. Media mount omitted; attach once bulk provisions.

Traces: R-GITOPS-02, R-NET-01/02/05, R-STATE-01/02/03, R-TLS/R-DNS,
R-OBS-01, R-UPD-01/03, R-IDP-02, R-PLAT-02.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 08:27:50 -05:00
jakman85 49229c65cf Merge feature/onboard-gitea: onboard Gitea (git hosting, DR-critical) via renderer 2026-08-25 08:24:21 -05:00
jakman85andClaude Opus 4.8 b76fb7bea7 feat(apps): onboard Gitea via ServiceDefinition renderer
Author catalog/gitea.yaml (kind: container, public, DR-critical git host per
docs/13 row 12), render to apps/gitea.yaml (--no-image-automation), and wire it
into apps/kustomization.yaml.

- image gitea/gitea:1.24.3, web port 3000 at gitea.home.keithjackson.dev
- 10Gi Longhorn PVC for /data (repos + SQLite DB), daily-offcluster backup
- exposure public, egress internet, auth oidc (platform-admins), metrics on
- Traces: R-STATE-02/03, R-NET-01/02, R-DNS/R-TLS, R-IDP-02/06, R-OBS-01, R-UPD-01/03

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 08:23:33 -05:00
jakman85 c70f99afd2 Merge docs/vip-bug-was-transient 2026-08-25 08:22:21 -05:00
jakman85 42fc8c7029 docs(pm): the Cilium VIP reachability bug was TRANSIENT - cleared by reboot, no CNI change needed 2026-08-25 08:22:21 -05:00
jakman85 1eb2cb70d0 Merge fix/trusted-tls-prod-verify (R-TLS-01/03 green on LE-prod) 2026-08-25 08:21:58 -05:00
jakman85 5ba3f9d46d fix(conformance): trusted-tls verifies LE-prod cert via system trust + split-horizon DNS (R-TLS-01/03)
Rewrote the probe for the LE-production switch: no embedded staging root, no --cacert
(system CA bundle validates ISRG Root X1), resolves the hostname via in-cluster
split-horizon DNS to the Gateway VIP (the real-client path). PASSES live (9s). Removed the
le-staging-root-ca fixture.
2026-08-25 08:21:55 -05:00
jakman85 90714cb910 Merge docs/cilium-vip-reachability-finding 2026-08-25 01:48:48 -05:00
jakman85 f977bcbee5 docs(pm): root-caused Cilium pod->Gateway-VIP reachability bug (non-announcer nodes) + gateway-ingress/pihole 2026-08-25 01:48:46 -05:00
jakman85 6522204478 Merge fix/gateway-ingress-default-deny: renderer emits gateway-ingress allow for exposed apps (R-NET-01/02/03) 2026-08-25 01:01:03 -05:00
jakman85andClaude Opus 4.8 c6a480eed1 test(net): L2 conformance — exposed app reachable through Gateway under default-deny (R-NET-03)
New runnable chainsaw test gateway-ingress-reachable (R-NET-01/02/03): stands up an
exposed-app fixture (namespace default-deny + the renderer's additive gateway-ingress
CiliumNetworkPolicy allowing fromEntities:[ingress] to the declared port 8080 only, a
backend listening on 8080 + an undeclared 9091, and declared/undeclared HTTPRoutes on
the shared Gateway) and proves BEHAVIOR through the REAL Gateway VIP:

  * then-reachable-through-gateway  -> the Gateway (ingress identity) reaches backend:8080
    through default-deny (backend marker returned)      [R-NET-02/03]
  * then-undeclared-port-denied     -> port 9091 (no allow) is NOT reachable through the
    Gateway (marker never returned; envoy 503)          [R-NET-02]
  * then-east-west-still-denied      -> a non-ingress in-namespace peer is DROPPED on 8080;
    the allow is least-privilege, east-west default-deny intact (hermetic)  [R-NET-01]

Verified live: reachable=REACHED (200, marker), undeclared=DENIED, east-west=DENIED.

Self-skipping: the gateway probes classify each attempt REACHED / DENIED / NO-GATEWAY
(curl 000). NO-GATEWAY self-skips (green + note) because a node-local Cilium L7LB/envoy
desync can make pods on an affected node unable to reach the Gateway VIP at all — a
SEPARATE platform fault, not default-deny — so this test never false-fails on it. The
east-west denial is hermetic and always asserts. VIP discovered per-step via a kubectl
output binding (env-agnostic; chainsaw bindings are step-scoped). L2 layer stays gated by
CLUSTER_AVAILABLE in run.sh. chainsaw v0.2.12 quirks honored (scalar/JMESPath asserts,
no readinessProbe under default-deny, step-level finally delete).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 00:13:35 -05:00
jakman85andClaude Opus 4.8 c13ef69e71 feat(net): renderer emits gateway-ingress allow for exposed services (R-NET-01/02)
Exposed apps got a default-deny NetworkPolicy with ingress: [] (deny all), so the
Cilium Gateway (Envoy, reserved identity `ingress`) could not reach the backend — an
Accepted HTTPRoute was still unreachable. render.py now emits an ADDITIVE, least-
privilege CiliumNetworkPolicy for any ServiceDefinition that renders an HTTPRoute:

  ingress:
    - fromEntities: [ingress]        # the shared Cilium Gateway/Envoy proxy identity
      toPorts: [<exposed service port(s)> TCP]

Verified against the live cluster (Cilium 1.16.5): with default-deny in place, Hubble
shows the Gateway proxy reaching pihole as source identity `(ingress)` and the flow
`Policy denied DROPPED` on :80 — so `fromEntities: [ingress]` is exactly the source the
Gateway uses, scoped to the service targetPort. Default-deny stays intact (ingress: []
unchanged); the allow only opens the north-south path (docs/09), east-west stays denied.

- render.py: add r_gateway_ingress_policy(); emit it after dependency policies; selftest
  asserts the allow is additive (default-deny ingress stays []), least-privilege (only the
  exposed port, only the ingress identity), and absent for non-exposed services.
- L1 goldens updated for the two exposed fixtures (container-workload, egress-class);
  default-deny-baseline golden unchanged (no exposure). --selftest + run.sh --layer l1 green.
- apps/pihole.yaml re-rendered (--no-image-automation) to carry the allow.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-24 23:40:52 -05:00
jakman85 e022a2aea4 Merge feature/onboard-pihole (first WS-APPS onboarding) 2026-08-24 23:27:22 -05:00
jakman85 e843db2601 feat(apps): onboard pihole via renderer + --no-image-automation flag (WS-APPS golden path)
First app onboarded through catalog/render.py -> apps/. Added --no-image-automation so apps
deploy before the Flux image controllers exist (L1 goldens keep the default, unchanged).
KNOWN GAP surfaced: exposed apps get default-deny (ingress: []) with no gateway-ingress allow,
so the HTTPRoute path is blocked — renderer/WS-NET follow-up to allow Cilium gateway ingress
to services with an exposure/HTTPRoute.
2026-08-24 23:27:22 -05:00
jakman85 34e4b191f1 Merge docs/session7-certified 2026-08-24 23:23:05 -05:00
jakman85 1cb8282f62 docs(pm): both deliverables certified — Keycloak SSO + KubeVirt P5 all green on KVM 2026-08-24 23:23:03 -05:00
jakman85 c05fd6702a Merge fix/kubevirt-vm-boot: real-KVM DataVolume VMs (R-PLAT-03/04 green; useEmulation:false, EFI, DataVolume roots) 2026-08-24 23:10:14 -05:00