Phase 5: KubeVirt + CDI IaaS proof (R-PLAT-03/04, R-STATE-01, R-NET-01)
- platform/kubevirt: pinned KubeVirt v1.9.0 + CDI v1.66.0 operators (remote bases); KubeVirt/CDI CRs tuned for Longhorn; useEmulation=true (prefer real KVM, emulate fallback). - Controllers/configs split (F-MON1): CRs + ServiceMonitors + alerts via platform-config. - Observability: ServiceMonitors, KubeVirt platform-health alerts (R-OBS-04), VM dashboard. - catalog/vm-demo.yaml (kind: vm) + rendered VirtualMachine/DataVolume fixture. - Conformance: vm-lifecycle (R-PLAT-03/04), vm-data-survival (R-STATE-01, L3), vm-default-deny (R-NET-01) — self-skipping until KubeVirt+longhorn exist. - ci/SPEC_PIN: adopt accepted spec b633b7f (ADR-0015 egress work already merged). - conformance/run.sh: document R-ENV-02/R-GITOPS-01 coverage gaps (WS-GITOPS/INFRA-owned). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
254d50a20c
commit
dbffb8fab8
16 files changed
+905
-61
No files matched your search
@@ -1,14 +1,26 @@
|
||||
# =============================================================================
|
||||
# L2 BEHAVIORAL SCAFFOLD — VM lifecycle (R-PLAT-03)
|
||||
# L2 BEHAVIORAL — VM lifecycle (R-PLAT-03, R-PLAT-04) [RUNNABLE / self-skips]
|
||||
# Catalog: docs/14 "VM lifecycle (R-PLAT-03, L2/L3)"
|
||||
# Spec: docs/06-vm-workloads-kubevirt.md, ADR-0005.
|
||||
#
|
||||
# GIVEN a `kind: vm` ServiceDefinition,
|
||||
# WHEN it is created/started/stopped/destroyed via kubectl/virtctl,
|
||||
# THEN it behaves through the same K8s surface; default-deny + egress class
|
||||
# apply to it. (Disk-survives-rebuild is the L3 partner test, see
|
||||
# tests/l3-fault-injection/data-survival — VMs share R-STATE-01.)
|
||||
# GIVEN a `kind: vm` workload (KubeVirt VirtualMachine),
|
||||
# WHEN it is created / started / stopped / restarted / destroyed via the SAME
|
||||
# Kubernetes API + kubectl surface as a container,
|
||||
# THEN each transition is honoured — the VMI reaches phase=Running on start,
|
||||
# disappears on stop, comes back on restart, and is gone after delete.
|
||||
# (R-PLAT-03 "VMs through the K8s API"; R-PLAT-04 "create/destroy/rebuild
|
||||
# on demand".) The Longhorn-disk-survives-rebuild partner proof is the L3
|
||||
# vm-data-survival test; the default-deny-applies-to-the-VM proof is the L2
|
||||
# vm-default-deny test — both share this VM model.
|
||||
#
|
||||
# STATUS: scaffold — needs T-5.1/T-5.2 (KubeVirt + `kind: vm` path).
|
||||
# RUNNABLE / SELF-SKIPPING: KubeVirt (Phase 5) may not be installed when the
|
||||
# broader suite runs. This test SKIPS CLEANLY (green) until the
|
||||
# virtualmachineinstances.kubevirt.io CRD exists, then runs the full lifecycle the
|
||||
# moment it does — mirroring the data-survival self-skip on `longhorn`. Everything
|
||||
# lives in the chainsaw per-test namespace and is torn down with it (hermetic). A
|
||||
# containerDisk root (kubelet-pulled, no CDI import) keeps the lifecycle proof fast
|
||||
# and free of image-import timing; the CDI-onto-Longhorn path is exercised by the
|
||||
# representative fixtures/vm.yaml via the deploy runbook and by vm-data-survival.
|
||||
# =============================================================================
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
@@ -16,31 +28,123 @@ metadata:
|
||||
name: vm-lifecycle
|
||||
labels:
|
||||
conformance.home.k3s/R-PLAT-03: "true"
|
||||
conformance.home.k3s/R-PLAT-04: "true"
|
||||
annotations:
|
||||
conformance.home.k3s/requirements: "R-PLAT-03"
|
||||
conformance.home.k3s/requirements: "R-PLAT-03, R-PLAT-04"
|
||||
conformance.home.k3s/layer: "L2"
|
||||
conformance.home.k3s/safety-critical: "false"
|
||||
conformance.home.k3s/executable: "true"
|
||||
conformance.home.k3s/intent: >-
|
||||
A VM is created/started/stopped/destroyed via the K8s API; policy applies.
|
||||
A VM is created/started/stopped/restarted/destroyed via the K8s API; the VMI
|
||||
honours each transition (Running on start, gone on delete). Skips cleanly
|
||||
until the KubeVirt CRDs exist.
|
||||
spec:
|
||||
# VM start pulls a containerDisk + boots a guest — well beyond the default 180s exec window.
|
||||
timeouts:
|
||||
exec: 20m
|
||||
steps:
|
||||
- name: given-vm-servicedefinition
|
||||
try:
|
||||
- apply:
|
||||
file: fixtures/vm-servicedefinition.yaml # TODO(T-5.2): author
|
||||
- name: when-start-and-assert-running
|
||||
- name: vm-lifecycle-through-k8s-api
|
||||
try:
|
||||
- script:
|
||||
content: |
|
||||
set -euo pipefail
|
||||
# virtctl start via the same kubectl surface; assert VMI Running.
|
||||
kubectl -n "$NAMESPACE" patch vm demo --type merge -p '{"spec":{"running":true}}'
|
||||
echo "TODO(T-5.3): wait VMI phase=Running (bounded), then STARTED_OK"
|
||||
- name: then-default-deny-and-egress-apply-to-vm
|
||||
try:
|
||||
NS="$NAMESPACE"
|
||||
|
||||
# -- Gate: skip cleanly (green) until KubeVirt is installed. --
|
||||
if ! kubectl get crd virtualmachineinstances.kubevirt.io >/dev/null 2>&1; then
|
||||
echo "SKIP: KubeVirt CRDs absent — vm-lifecycle not applicable on this cluster yet"
|
||||
echo VM_LIFECYCLE_SKIPPED_OK
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# -- GIVEN: a stopped VirtualMachine, defined declaratively (containerDisk root). --
|
||||
kubectl -n "$NS" apply -f - <<'EOF'
|
||||
apiVersion: kubevirt.io/v1
|
||||
kind: VirtualMachine
|
||||
metadata:
|
||||
name: lc
|
||||
spec:
|
||||
running: false
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
kubevirt.io/vm: lc
|
||||
homelab.keithjackson.dev/egress-class: internet
|
||||
spec:
|
||||
domain:
|
||||
cpu: { cores: 1 }
|
||||
memory: { guest: 1024Mi }
|
||||
devices:
|
||||
disks:
|
||||
- name: rootdisk
|
||||
disk: { bus: virtio }
|
||||
interfaces:
|
||||
- name: default
|
||||
masquerade: {}
|
||||
resources:
|
||||
requests:
|
||||
memory: 1024Mi
|
||||
networks:
|
||||
- name: default
|
||||
pod: {}
|
||||
volumes:
|
||||
- name: rootdisk
|
||||
containerDisk:
|
||||
image: quay.io/containerdisks/fedora:41
|
||||
EOF
|
||||
# No VMI should exist while running=false.
|
||||
if kubectl -n "$NS" get vmi lc >/dev/null 2>&1; then
|
||||
echo "FAIL: VMI exists though the VM is defined stopped" >&2; exit 1
|
||||
fi
|
||||
echo "created VM 'lc' (stopped)"
|
||||
|
||||
wait_phase() { # $1=expected phase, bounded poll
|
||||
local want="$1" i phase
|
||||
for i in $(seq 1 80); do
|
||||
phase="$(kubectl -n "$NS" get vmi lc -o jsonpath='{.status.phase}' 2>/dev/null || true)"
|
||||
[ "$phase" = "$want" ] && return 0
|
||||
sleep 10
|
||||
done
|
||||
echo "FAIL: VMI did not reach phase=$want (last='$phase')" >&2; return 1
|
||||
}
|
||||
wait_gone() {
|
||||
local i
|
||||
for i in $(seq 1 30); do
|
||||
kubectl -n "$NS" get vmi lc >/dev/null 2>&1 || return 0
|
||||
sleep 5
|
||||
done
|
||||
echo "FAIL: VMI still present when it should be gone" >&2; return 1
|
||||
}
|
||||
|
||||
# -- WHEN start (running=true) THEN VMI reaches Running. --
|
||||
kubectl -n "$NS" patch vm lc --type merge -p '{"spec":{"running":true}}'
|
||||
wait_phase Running
|
||||
echo "STARTED: VMI phase=Running"
|
||||
|
||||
# -- WHEN stop (running=false) THEN the VMI is torn down. --
|
||||
kubectl -n "$NS" patch vm lc --type merge -p '{"spec":{"running":false}}'
|
||||
wait_gone
|
||||
echo "STOPPED: VMI removed"
|
||||
|
||||
# -- WHEN restart (running=true again) THEN it comes back Running (rebuild-on-demand). --
|
||||
kubectl -n "$NS" patch vm lc --type merge -p '{"spec":{"running":true}}'
|
||||
wait_phase Running
|
||||
echo "RESTARTED: VMI phase=Running"
|
||||
|
||||
# -- WHEN destroy (delete the VM) THEN both VM and VMI are gone. --
|
||||
kubectl -n "$NS" delete vm lc --wait=true --timeout=300s
|
||||
wait_gone
|
||||
if kubectl -n "$NS" get vm lc >/dev/null 2>&1; then
|
||||
echo "FAIL: VM object still present after delete" >&2; exit 1
|
||||
fi
|
||||
echo "DESTROYED: VM + VMI gone"
|
||||
|
||||
echo VM_LIFECYCLE_OK
|
||||
check:
|
||||
# green on a clean skip (no KubeVirt) OR a proven full lifecycle.
|
||||
(contains($stdout, 'VM_LIFECYCLE_OK') || contains($stdout, 'VM_LIFECYCLE_SKIPPED_OK')): true
|
||||
finally:
|
||||
- script:
|
||||
content: 'echo "TODO: run default-deny + egress-class probes from inside the VM"'
|
||||
- name: when-stop-and-destroy
|
||||
try:
|
||||
- script:
|
||||
content: 'echo "TODO(T-5.3): stop then delete VM via kubectl; assert gone"'
|
||||
content: |
|
||||
kubectl -n "$NAMESPACE" delete vm lc --ignore-not-found --wait=false || true
|
||||
echo "cleanup best-effort; chainsaw namespace teardown finalizes"
|
||||
@@ -0,0 +1,94 @@
|
||||
# Rendered from catalog/vm-demo.yaml (kind: vm) — the Phase-5 IaaS proof workload (ADR-0005, docs/06).
|
||||
# A small headless Fedora Cloud VM whose root disk is CDI-imported onto a Longhorn PVC. Applied by the
|
||||
# vm-lifecycle (R-PLAT-03) test and by the deploy runbook. Namespace-agnostic: chainsaw injects its
|
||||
# ephemeral namespace; the runbook applies it into a `vm-demo` namespace (see platform/kubevirt/README).
|
||||
#
|
||||
# DURABILITY (R-STATE-01): the DataVolume is STANDALONE (not a `dataVolumeTemplates` entry), so it is
|
||||
# NOT owner-referenced by the VirtualMachine and OUTLIVES a `kubectl delete vm`. Destroy the VM, keep
|
||||
# this DataVolume/PVC, re-apply the VM — it re-binds the same Longhorn disk. (dataVolumeTemplates would
|
||||
# be garbage-collected with the VM and would FAIL rebuild-survival.)
|
||||
---
|
||||
apiVersion: cdi.kubevirt.io/v1beta1
|
||||
kind: DataVolume
|
||||
metadata:
|
||||
name: vm-demo-rootdisk
|
||||
spec:
|
||||
source:
|
||||
registry:
|
||||
# CDI imports this containerdisk (Fedora Cloud) into the Longhorn PVC below. Pinned tag; Renovate
|
||||
# can maintain. Import needs egress to quay.io — in an app namespace under default-deny the runbook
|
||||
# adds a CiliumNetworkPolicy allowing the CDI importer pod egress (see README); the ephemeral test
|
||||
# namespace has no default-deny baseline, so the import egresses freely there.
|
||||
url: "docker://quay.io/containerdisks/fedora:41"
|
||||
storage:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn # replicated tier (ADR-0003) — VM disk gets the same durability
|
||||
resources:
|
||||
requests:
|
||||
storage: 6Gi
|
||||
---
|
||||
apiVersion: kubevirt.io/v1
|
||||
kind: VirtualMachine
|
||||
metadata:
|
||||
name: vm-demo
|
||||
labels:
|
||||
app.kubernetes.io/name: vm-demo
|
||||
spec:
|
||||
# Lifecycle is driven through the K8s API: `running: false` here means "defined but stopped"; the
|
||||
# test/operator sets it true/false to start/stop, and `kubectl delete vm` destroys it (R-PLAT-03/04).
|
||||
running: false
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
kubevirt.io/vm: vm-demo
|
||||
# Propagated to the virt-launcher pod so Cilium egress steering + the Kyverno egress-class
|
||||
# policy see the class (ADR-0015). internet = default WAN path (this proof is not VPN-class).
|
||||
homelab.keithjackson.dev/egress-class: internet
|
||||
spec:
|
||||
domain:
|
||||
cpu:
|
||||
cores: 1
|
||||
memory:
|
||||
guest: 1536Mi
|
||||
devices:
|
||||
disks:
|
||||
- name: rootdisk
|
||||
disk:
|
||||
bus: virtio
|
||||
- name: cloudinit
|
||||
disk:
|
||||
bus: virtio
|
||||
interfaces:
|
||||
- name: default
|
||||
masquerade: {}
|
||||
resources:
|
||||
requests:
|
||||
memory: 1536Mi
|
||||
networks:
|
||||
- name: default
|
||||
pod: {}
|
||||
volumes:
|
||||
- name: rootdisk
|
||||
dataVolume:
|
||||
name: vm-demo-rootdisk
|
||||
- name: cloudinit
|
||||
cloudInitNoCloud:
|
||||
# Guest auto-updates with NO human step (docs/11 §3, R-UPD-01): enable dnf-automatic so the
|
||||
# guest applies stable package updates on its own timer. This is the VM analogue of a
|
||||
# container image-policy bump — the update policy is declared once, automation applies it.
|
||||
userData: |-
|
||||
#cloud-config
|
||||
package_update: true
|
||||
packages:
|
||||
- dnf-automatic
|
||||
write_files:
|
||||
- path: /etc/dnf/automatic.conf
|
||||
content: |
|
||||
[commands]
|
||||
upgrade_type = default
|
||||
random_sleep = 0
|
||||
download_updates = yes
|
||||
apply_updates = yes
|
||||
runcmd:
|
||||
- [ systemctl, enable, --now, dnf-automatic.timer ]
|
||||
Reference in new issue
Block a user