# ============================================================================= # 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` 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. # # 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 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, 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/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: vm-lifecycle-through-k8s-api try: - script: content: | set -euo pipefail 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: | kubectl -n "$NAMESPACE" delete vm lc --ignore-not-found --wait=false || true echo "cleanup best-effort; chainsaw namespace teardown finalizes"