Files
cozystack/hack/testdata/migration-59-vmi-hotplug/kubectl
Aleksei Sviridkin fd8ccb2fcb feat(platform): keep VMI-only hotplugged volumes across declarative hotplug
Dropping the HotplugVolumes gate lets virt-controller rebuild each
running VMI's hotplug volumes from VM.spec, and it removes every
hotpluggable VMI volume the VM spec does not list. kubevirt-csi
attached tenant volumes at the VMI level until mid-2025 and live
migration carries such a VMI across KubeVirt updates, so a tenant node
that was never recreated can still hold a mounted disk only its VMI
knows about; the first reconcile after the upgrade would unplug it.

Migration 59 writes each such volume and its disk into the VM template
as they are on the VMI, before the upgrade re-points cozystack-packages
and so before the KubeVirt CR and the kubevirt-csi flag change. Hotplug
volume changes are a valid live update, so no VM needs a restart.

An ephemeral hotplug a user made at the VMI level, such as virtctl
addvolume without --persist, has the same shape, because virt-api gives
every VMI-level addvolume that shape. It is copied too and becomes
permanent: leaving it out would not keep it ephemeral, since the first
reconcile would unplug it. Only Running VMIs are considered, matching
virt-controller, which reconciles no other; a stopped VM's next start
builds its VMI from the VM spec, so its VMI-only volumes are not
resurrected.

Only the variants with migrations enabled run it. The default variant
renders no migration hook, so a cluster there that installed KubeVirt
by hand has to check for such volumes itself before upgrading.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-28 23:37:48 +03:00

94 lines
4.0 KiB
Bash
Executable File

#!/bin/sh
# Fake kubectl for hack/migration-59-vmi-hotplug-volumes.bats. It serves only
# the calls migration 59 and lib/cozystack-version.sh make, and logs each one
# to $FAKE_CMDLOG.
#
# The VMs live in $FAKE_STATE/vms.json and a JSON patch is applied to that
# file, so a second run of the migration reads what the first one wrote.
#
# FAKE_CMDLOG file to append a one-line record of each call to
# FAKE_STATE directory holding vms.json and vmis.json
# FAKE_NO_KUBEVIRT non-empty => the VirtualMachine CRD is absent
# FAKE_CRD_FAIL non-empty => reading the VirtualMachine CRD exits 1
# FAKE_LIST_FAIL non-empty => listing VMs exits 1 with this stderr
# FAKE_VMI_LIST_FAIL non-empty => listing VMIs exits 1 with this stderr
# FAKE_PATCH_FAIL non-empty => every patch exits 1 with this stderr
set -u
log() { [ -n "${FAKE_CMDLOG:-}" ] && printf '%s\n' "$*" >> "$FAKE_CMDLOG"; }
log "KUBECTL $*"
args="$*"
arg_after() { k="$1"; shift; p=""; for a in "$@"; do [ "$p" = "$k" ] && { printf '%s' "$a"; return; }; p="$a"; done; }
case "$args" in
*"apply --filename -"*|*"apply -f -"*)
manifest=$(cat)
case "$manifest" in
*"kind: ConfigMap"*)
v=$(printf '%s\n' "$manifest" | sed -n 's/^ version: "\{0,1\}\([0-9]\{1,\}\)"\{0,1\}$/\1/p' | head -1)
log "STAMP ${v:-unknown}"
exit 0
;;
esac
log "APPLY-UNKNOWN"
printf 'fake kubectl: unmodelled apply\n' >&2
exit 1
;;
*"get crd virtualmachines.kubevirt.io"*)
if [ -n "${FAKE_CRD_FAIL:-}" ]; then printf '%s\n' "$FAKE_CRD_FAIL" >&2; exit 1; fi
[ -n "${FAKE_NO_KUBEVIRT:-}" ] || echo "customresourcedefinition.apiextensions.k8s.io/virtualmachines.kubevirt.io"
exit 0
;;
*"get virtualmachines.kubevirt.io"*)
if [ -n "${FAKE_LIST_FAIL:-}" ]; then printf '%s\n' "$FAKE_LIST_FAIL" >&2; exit 1; fi
cat "$FAKE_STATE/vms.json" || exit 1
exit 0
;;
*"get virtualmachineinstances.kubevirt.io"*)
if [ -n "${FAKE_VMI_LIST_FAIL:-}" ]; then printf '%s\n' "$FAKE_VMI_LIST_FAIL" >&2; exit 1; fi
cat "$FAKE_STATE/vmis.json" || exit 1
exit 0
;;
*"patch virtualmachines.kubevirt.io"*)
if [ -n "${FAKE_PATCH_FAIL:-}" ]; then printf '%s\n' "$FAKE_PATCH_FAIL" >&2; exit 1; fi
ns=$(arg_after -n "$@")
name=$(arg_after virtualmachines.kubevirt.io "$@")
patch=$(arg_after -p "$@")
# Refuse a stale resourceVersion the way the API server does, then apply
# every add op and bump the resourceVersion.
rv=$(jq -r --arg ns "$ns" --arg n "$name" \
'.items[] | select(.metadata.namespace == $ns and .metadata.name == $n) | .metadata.resourceVersion' \
"$FAKE_STATE/vms.json")
want=$(printf '%s' "$patch" | jq -r '.[] | select(.op == "test" and .path == "/metadata/resourceVersion") | .value')
if [ -z "$want" ] || [ "$want" != "$rv" ]; then
printf 'Error from server (Conflict): test failed\n' >&2
exit 1
fi
jq --arg ns "$ns" --arg n "$name" --argjson p "$patch" '
.items |= map(
if .metadata.namespace == $ns and .metadata.name == $n then
reduce ($p[] | select(.op == "add")) as $op (.;
setpath($op.path | ltrimstr("/") | split("/"); $op.value))
| .metadata.resourceVersion = ((.metadata.resourceVersion | tonumber) + 1 | tostring)
else . end)' "$FAKE_STATE/vms.json" > "$FAKE_STATE/vms.json.new"
mv "$FAKE_STATE/vms.json.new" "$FAKE_STATE/vms.json"
vols=$(jq -r --arg ns "$ns" --arg n "$name" \
'.items[] | select(.metadata.namespace == $ns and .metadata.name == $n) | [.spec.template.spec.volumes[].name] | join(",")' \
"$FAKE_STATE/vms.json")
disks=$(jq -r --arg ns "$ns" --arg n "$name" \
'.items[] | select(.metadata.namespace == $ns and .metadata.name == $n) | [.spec.template.spec.domain.devices.disks[].name] | join(",")' \
"$FAKE_STATE/vms.json")
log "PATCH-VM $ns/$name volumes=$vols disks=$disks"
exit 0
;;
esac
log "UNHANDLED $args"
printf 'fake kubectl: unmodelled call: %s\n' "$args" >&2
exit 1