Skip to content

Commit 732261e

Browse files
androndoAndrey
authored andcommitted
test(vmdisk): add standalone VMDisk backup/restore e2e round-trip
cozy-default binds VMDisk to a Velero strategy (cozy-default-velero-vmdisk) just as it does VMInstance, but that path was never exercised: the vminstance suite only tested standalone VMDisk creation, and the VMInstance round-trip backs up a disk attached to a running VM, not a standalone one. A standalone VMDisk is a distinct path — the strategy captures only the HelmRelease + PVC (+ configmaps), with no VirtualMachine / VMI / launcher pod. Add a self-contained example (examples/backups/vmdisk, mirroring the etcd/postgres harness style) driven by a new gated Chainsaw Test, vmdisk-2-backup-roundtrip, in the vminstance suite: Velero strategy -> BackupClass -> an imported 5Gi VMDisk -> ad-hoc BackupJob (waits Succeeded) -> in-place RestoreJob (waits Succeeded, then waits for the restored disk to be Bound + Ready). A standalone disk is not mounted anywhere, so the round-trip proof is that the restored VMDisk reconciles back to a Ready HelmRelease + Bound PVC; in-guest data is exercised by vminstance-2-backup-roundtrip, which boots the VM from its restored disk. Runs in tenant-root and reuses the VMI_E2E_S3_ROUNDTRIP=1 gate (same self-signed-S3 / BSL-defaults blocker as the VMInstance round-trip, #3236), with skipDelete + an explicit finally that prunes the cluster-scoped BackupClass and strategy, the cozy-velero Velero Backup/Restore, the VMDisk and any renamed original PVC. Assisted-by: LLM Signed-off-by: Andrey Kolkov <[email protected]>
1 parent d0c6345 commit 732261e

9 files changed

Lines changed: 468 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/bin/bash
2+
# Helper functions and variables for the VMDisk backup/restore demo.
3+
# Source this file in other scripts: source "$(dirname "$0")/00-helpers.sh"
4+
5+
# ANSI color codes
6+
export RED='\033[0;31m'
7+
export GREEN='\033[0;32m'
8+
export YELLOW='\033[1;33m'
9+
export MAGENTA='\033[0;35m'
10+
export CYAN='\033[0;36m'
11+
export WHITE='\033[1;37m'
12+
export NC='\033[0m'
13+
export BOLD='\033[1m'
14+
15+
# Default settings
16+
export NAMESPACE="${NAMESPACE:-tenant-root}"
17+
# The platform ships the SeaweedFS-backed BackupStorageLocation as cozy-default
18+
# (packages/system/backupstrategy-controller). Point the demo/e2e strategy at it
19+
# by default so the flow works out of the box on a backups-enabled cluster;
20+
# override for an external S3 whose BSL carries a different name.
21+
export BACKUP_STORAGE_LOCATION="${BACKUP_STORAGE_LOCATION:-cozy-default}"
22+
23+
# Logging functions (output to stderr to avoid polluting captured output)
24+
log_info() { echo -e "${CYAN}ℹ${NC} $*" >&2; }
25+
log_success() { echo -e "${GREEN}✔${NC} $*" >&2; }
26+
log_warning() { echo -e "${YELLOW}⚠${NC} $*" >&2; }
27+
log_error() { echo -e "${RED}✖${NC} $*" >&2; }
28+
log_step() { echo -e "\n${MAGENTA}${BOLD}▶ $*${NC}" >&2; }
29+
log_command() { echo -e "${WHITE} \$ $*${NC}" >&2; }
30+
31+
separator() {
32+
echo -e "\n${CYAN}────────────────────────────────────────────────────────────${NC}\n" >&2
33+
}
34+
35+
print_header() {
36+
local title="$1"
37+
echo -e "\n${MAGENTA}${BOLD}== ${title} ==${NC}\n" >&2
38+
}
39+
40+
# Wait for a resource field to reach a desired value.
41+
# Args: type name jsonpath desired [namespace] [timeout] [fail_value]
42+
wait_for_field() {
43+
local resource_type="$1"
44+
local resource_name="$2"
45+
local jsonpath="$3"
46+
local desired="$4"
47+
local namespace="${5:-}"
48+
local timeout="${6:-300}"
49+
# Optional terminal-failure value: when the field reaches it, stop
50+
# immediately instead of polling out the timeout. Fail fast, fail loud.
51+
local fail_value="${7:-}"
52+
53+
log_command "waiting for $resource_type/$resource_name $jsonpath == '$desired'"
54+
55+
local elapsed=0
56+
local ns_flag=""
57+
[[ -n "$namespace" ]] && ns_flag="-n $namespace"
58+
59+
while true; do
60+
local current
61+
# shellcheck disable=SC2086
62+
current=$(kubectl get "$resource_type" "$resource_name" $ns_flag -o jsonpath="$jsonpath" 2>/dev/null || true)
63+
if [[ "$current" == "$desired" ]]; then
64+
log_success "$resource_type/$resource_name reached '$desired'"
65+
return 0
66+
fi
67+
if [[ -n "$fail_value" && "$current" == "$fail_value" ]]; then
68+
log_error "$resource_type/$resource_name reached terminal failure state '$fail_value'"
69+
return 1
70+
fi
71+
if [[ $elapsed -ge $timeout ]]; then
72+
log_error "Timeout waiting for $resource_type/$resource_name (current: '$current', expected: '$desired')"
73+
return 1
74+
fi
75+
sleep 5
76+
elapsed=$((elapsed + 5))
77+
echo -n "." >&2
78+
done
79+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/bin/bash
2+
# Step 01: Create the Velero backup/restore strategy for a standalone VMDisk.
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/00-helpers.sh"
7+
8+
print_header "Step 1: Create VMDisk Velero strategy"
9+
10+
log_step "Creating Velero strategy 'vmdisk-backup-strategy'..."
11+
log_command "kubectl apply -f - (Velero strategy: vmdisk-backup-strategy)"
12+
13+
# A standalone VMDisk is just an HelmRelease + a DataVolume-backed PVC (+ its
14+
# configmaps), all labelled app.kubernetes.io/instance: vm-disk-<name>. Unlike a
15+
# VMInstance there is no VirtualMachine / VMI / launcher pod to capture. With
16+
# snapshotMoveData the CSI data mover streams the volume through the
17+
# BackupStorageLocation, so no VolumeSnapshotLocation is required (matching the
18+
# platform's own cozy-default-velero-vmdisk strategy).
19+
kubectl apply -f - <<'EOF'
20+
apiVersion: strategy.backups.cozystack.io/v1alpha1
21+
kind: Velero
22+
metadata:
23+
name: vmdisk-backup-strategy
24+
spec:
25+
template:
26+
restoreSpec:
27+
existingResourcePolicy: update
28+
includedNamespaces:
29+
- '{{ .Application.metadata.namespace }}'
30+
orLabelSelectors:
31+
- matchLabels:
32+
app.kubernetes.io/instance: 'vm-disk-{{ .Application.metadata.name }}'
33+
- matchLabels:
34+
apps.cozystack.io/application.kind: '{{ .Application.kind }}'
35+
apps.cozystack.io/application.name: '{{ .Application.metadata.name }}'
36+
includedResources:
37+
- helmreleases.helm.toolkit.fluxcd.io
38+
- persistentvolumeclaims
39+
- configmaps
40+
includeClusterResources: false
41+
spec:
42+
includedNamespaces:
43+
- '{{ .Application.metadata.namespace }}'
44+
orLabelSelectors:
45+
- matchLabels:
46+
app.kubernetes.io/instance: 'vm-disk-{{ .Application.metadata.name }}'
47+
- matchLabels:
48+
apps.cozystack.io/application.kind: '{{ .Application.kind }}'
49+
apps.cozystack.io/application.name: '{{ .Application.metadata.name }}'
50+
includedResources:
51+
- helmreleases.helm.toolkit.fluxcd.io
52+
- persistentvolumeclaims
53+
- configmaps
54+
includeClusterResources: false
55+
storageLocation: '{{ .Parameters.backupStorageLocationName }}'
56+
snapshotVolumes: true
57+
snapshotMoveData: true
58+
ttl: 720h0m0s
59+
itemOperationTimeout: 24h0m0s
60+
EOF
61+
62+
log_success "Velero strategy created"
63+
echo -e "\n${GREEN}${BOLD}Next step:${NC} ./02-create-backupclass.sh" >&2
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
# Step 02: Create a BackupClass binding VMDisk to the Velero strategy.
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/00-helpers.sh"
7+
8+
print_header "Step 2: Create BackupClass"
9+
10+
log_step "Creating BackupClass 'vmdisk-velero'..."
11+
log_command "kubectl apply -f - (BackupClass: vmdisk-velero)"
12+
13+
# BackupClass is cluster-scoped; a single instance covers every namespace.
14+
kubectl apply -f - <<EOF
15+
apiVersion: backups.cozystack.io/v1alpha1
16+
kind: BackupClass
17+
metadata:
18+
name: vmdisk-velero
19+
spec:
20+
strategies:
21+
- application:
22+
apiGroup: apps.cozystack.io
23+
kind: VMDisk
24+
strategyRef:
25+
apiGroup: strategy.backups.cozystack.io
26+
kind: Velero
27+
name: vmdisk-backup-strategy
28+
parameters:
29+
backupStorageLocationName: ${BACKUP_STORAGE_LOCATION}
30+
EOF
31+
32+
log_success "BackupClass created"
33+
echo -e "\n${GREEN}${BOLD}Next step:${NC} ./03-create-vmdisk.sh" >&2
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
# Step 03: Create a standalone VMDisk and wait for it to be ready to back up.
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/00-helpers.sh"
7+
8+
print_header "Step 3: Create VMDisk 'backup-src'"
9+
10+
log_step "Creating VMDisk 'backup-src' in namespace $NAMESPACE..."
11+
log_info "Imports the Ubuntu Noble cloud image into a 5Gi replicated disk"
12+
log_command "kubectl apply -f - (VMDisk: backup-src)"
13+
14+
kubectl apply -f - <<EOF
15+
apiVersion: apps.cozystack.io/v1alpha1
16+
kind: VMDisk
17+
metadata:
18+
name: backup-src
19+
namespace: ${NAMESPACE}
20+
spec:
21+
optical: false
22+
source:
23+
http:
24+
url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
25+
storage: 5Gi
26+
storageClass: replicated
27+
EOF
28+
29+
log_success "VMDisk created"
30+
31+
# Back up only a fully imported disk: the DataVolume import must finish and the
32+
# PVC bind before the backup runs, or Velero captures an empty/partial volume.
33+
log_step "Waiting for the VMDisk to import (HelmRelease Ready, DataVolume Ready, PVC Bound)..."
34+
kubectl -n "$NAMESPACE" wait hr vm-disk-backup-src --for=condition=ready --timeout=600s
35+
wait_for_field datavolume.cdi.kubevirt.io vm-disk-backup-src \
36+
'{.status.phase}' Succeeded "$NAMESPACE" 600
37+
wait_for_field pvc vm-disk-backup-src '{.status.phase}' Bound "$NAMESPACE" 300
38+
39+
log_success "VMDisk 'backup-src' is imported and ready to back up"
40+
echo -e "\n${GREEN}${BOLD}Next step:${NC} ./04-create-backupjob.sh" >&2
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
# Step 04: Back up the standalone VMDisk via a BackupJob.
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/00-helpers.sh"
7+
8+
print_header "Step 4: Create BackupJob"
9+
10+
log_step "Creating BackupJob 'vmdisk-backup' in namespace $NAMESPACE..."
11+
log_command "kubectl apply -f - (BackupJob: vmdisk-backup)"
12+
13+
kubectl apply -f - <<EOF
14+
apiVersion: backups.cozystack.io/v1alpha1
15+
kind: BackupJob
16+
metadata:
17+
name: vmdisk-backup
18+
namespace: ${NAMESPACE}
19+
spec:
20+
applicationRef:
21+
apiGroup: apps.cozystack.io
22+
kind: VMDisk
23+
name: backup-src
24+
backupClassName: vmdisk-velero
25+
EOF
26+
27+
log_success "BackupJob created"
28+
29+
log_step "Waiting for BackupJob to complete..."
30+
# 1800s: the Velero CSI data mover copies the whole disk (5Gi) to S3. Fail fast
31+
# if the job flips to Failed instead of polling out the budget.
32+
wait_for_field backupjob vmdisk-backup '{.status.phase}' Succeeded "$NAMESPACE" 1800 Failed
33+
34+
BACKUP_NAME=$(kubectl -n "$NAMESPACE" get backupjob vmdisk-backup \
35+
-o jsonpath='{.status.backupRef.name}')
36+
[[ -n "$BACKUP_NAME" ]] || { log_error "BackupJob succeeded but reported no backupRef"; exit 1; }
37+
log_success "BackupJob completed; backup artefact: ${BACKUP_NAME}"
38+
39+
kubectl get backups -n "$NAMESPACE" >&2 || true
40+
echo -e "\n${GREEN}${BOLD}Next step:${NC} ./05-restore-in-place.sh" >&2
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
# Step 05: Restore the standalone VMDisk in-place from its backup.
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
source "$SCRIPT_DIR/00-helpers.sh"
7+
8+
print_header "Step 5: Restore VMDisk In-Place"
9+
10+
log_step "Creating RestoreJob 'vmdisk-restore-in-place' in namespace $NAMESPACE..."
11+
log_info "In-place restore: the original PVC is renamed and the disk is restored from backup"
12+
log_command "kubectl apply -f - (RestoreJob: vmdisk-restore-in-place)"
13+
14+
kubectl apply -f - <<EOF
15+
apiVersion: backups.cozystack.io/v1alpha1
16+
kind: RestoreJob
17+
metadata:
18+
name: vmdisk-restore-in-place
19+
namespace: ${NAMESPACE}
20+
spec:
21+
backupRef:
22+
name: vmdisk-backup
23+
targetApplicationRef:
24+
apiGroup: apps.cozystack.io
25+
kind: VMDisk
26+
name: backup-src
27+
options:
28+
failIfTargetExists: true # fail if the restore target already exists
29+
keepOriginalPVC: true # rename the original PVC to <name>-orig-<hash> before restoring
30+
EOF
31+
32+
log_success "RestoreJob created"
33+
34+
log_step "Waiting for RestoreJob to complete..."
35+
wait_for_field restorejob vmdisk-restore-in-place '{.status.phase}' Succeeded "$NAMESPACE" 1800 Failed
36+
37+
# The RestoreJob reaching Succeeded means Velero re-created the objects; prove the
38+
# restored VMDisk reconciles back to a Bound, ready disk. A standalone disk is not
39+
# mounted anywhere, so a Ready HelmRelease + Bound PVC is the round-trip proof
40+
# (in-guest data is exercised by the VMInstance round-trip, which boots the VM).
41+
log_step "Waiting for the restored VMDisk to become Ready again..."
42+
kubectl -n "$NAMESPACE" wait hr vm-disk-backup-src --for=condition=ready --timeout=600s
43+
wait_for_field pvc vm-disk-backup-src '{.status.phase}' Bound "$NAMESPACE" 300
44+
45+
kubectl get restorejob vmdisk-restore-in-place -n "$NAMESPACE" -o wide >&2 || true
46+
log_success "In-place restore completed; VMDisk 'backup-src' is Bound and Ready"

‎examples/backups/vmdisk/cleanup.sh‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
# Clean up all resources created by the demo. Non-interactive: safe to call from
3+
# the e2e harness's finally block, and idempotent (a no-op when a gated-out run
4+
# created nothing). --ignore-not-found returns success for an absent resource but
5+
# does NOT mask a stuck delete: the delete still blocks and, under set -e, a
6+
# teardown that never settles fails this script instead of leaking state into the
7+
# next run (docs/agents/e2e-testing.md convention 4).
8+
set -euo pipefail
9+
10+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11+
source "$SCRIPT_DIR/00-helpers.sh"
12+
13+
print_header "Cleanup Demo Resources"
14+
15+
# Namespaced request objects first. Deleting the cozystack BackupJob/RestoreJob
16+
# and Backup CRs cascades to the Velero Backup/Restore the controller created.
17+
log_step "Deleting RestoreJob..."
18+
kubectl delete restorejob vmdisk-restore-in-place -n "$NAMESPACE" --ignore-not-found
19+
20+
log_step "Deleting BackupJob and Backup..."
21+
kubectl delete backupjob vmdisk-backup -n "$NAMESPACE" --ignore-not-found
22+
kubectl delete backup vmdisk-backup -n "$NAMESPACE" --ignore-not-found
23+
24+
separator
25+
26+
log_step "Deleting VMDisk (and any renamed original PVC left by the restore)..."
27+
kubectl delete vmdisk backup-src -n "$NAMESPACE" --ignore-not-found
28+
kubectl delete pvc -n "$NAMESPACE" \
29+
-l app.kubernetes.io/instance=vm-disk-backup-src --ignore-not-found
30+
31+
separator
32+
33+
log_step "Deleting BackupClass and Velero strategy..."
34+
kubectl delete backupclass vmdisk-velero --ignore-not-found
35+
kubectl delete velero.strategy.backups.cozystack.io vmdisk-backup-strategy --ignore-not-found
36+
37+
separator
38+
39+
log_success "Cleanup complete"

‎examples/backups/vmdisk/run-all.sh‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
# Convenience runner + e2e harness for the standalone VMDisk backup/restore demo.
3+
#
4+
# Runs the numbered steps 01..05 a human reads, in order, non-interactively,
5+
# stopping on the first failure (set -e). This is the SAME sequence the docs walk
6+
# through, so the documented demo and the automated test can never drift.
7+
#
8+
# Flow: Velero strategy -> BackupClass -> a standalone VMDisk (waits imported) ->
9+
# ad-hoc BackupJob (waits Succeeded) -> in-place RestoreJob (waits Succeeded, then
10+
# waits for the restored disk to be Bound + Ready).
11+
#
12+
# Override NAMESPACE / BACKUP_STORAGE_LOCATION via the environment; see
13+
# 00-helpers.sh. hack/e2e-chainsaw/vminstance/ drives this file as the gated
14+
# vmdisk-2-backup-roundtrip test (VMI_E2E_S3_ROUNDTRIP=1).
15+
set -euo pipefail
16+
17+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18+
source "$SCRIPT_DIR/00-helpers.sh"
19+
20+
print_header "VMDisk Backup & Restore Demo"
21+
log_info "Namespace: ${NAMESPACE} BackupStorageLocation: ${BACKUP_STORAGE_LOCATION}"
22+
23+
SCRIPTS=(
24+
"01-create-strategy.sh"
25+
"02-create-backupclass.sh"
26+
"03-create-vmdisk.sh"
27+
"04-create-backupjob.sh"
28+
"05-restore-in-place.sh"
29+
)
30+
31+
for script in "${SCRIPTS[@]}"; do
32+
if [[ -x "$SCRIPT_DIR/$script" ]]; then
33+
"$SCRIPT_DIR/$script"
34+
separator
35+
else
36+
log_error "Script not found or not executable: $script"
37+
exit 1
38+
fi
39+
done
40+
41+
print_header "Demo Complete"
42+
log_success "All steps completed successfully!"
43+
log_info "To clean up resources: ./cleanup.sh"

0 commit comments

Comments
 (0)