Skip to content

Commit 1c90e1e

Browse files
kvapsclaude
andcommitted
test(e2e): scaffold 12 deterministic e2e scenarios (Phase 8.8)
Each scenario captures one Phase 8 invariant as an automatable shell script with explicit FAIL exits. Shared lib.sh keeps each scenario focused on the test logic rather than boilerplate. Scenarios cover: - evacuate: node evacuate triggers replica migration - network-partition: 1-vs-2 partition, fence + bitmap merge on heal - backing-device-fail: events2 observer auto-detach on disk:Failed - snapshot-restore-cross-node + clone: data-preserving restore on a different node pair - resize-luks + resize-plain: provider + cryptsetup + drbdadm chain - two-primaries-live-migration: allow-two-primaries plumbing in .res - rwx-ganesha + two-volume-rd: multi-volume RD shape + drbd-reactor hooks - tiebreaker: ResourceDefinitionReconciler witness creation - auto-diskful: ResourceReconciler DISKLESS → diskful promotion `make e2e NAME=<cluster> SCENARIO=<name>` runs one; `make e2e-list` enumerates them. Stand-side runs and any failures the scenarios surface are tracked under Phase 8.6/8.7. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 3ffa4de commit 1c90e1e

15 files changed

Lines changed: 1184 additions & 20 deletions

PLAN.md

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -431,13 +431,7 @@ The phases above closed the MVP slice and the csi-sanity REST contract. A deep a
431431
- `piraeus-ha-controller` ([upstream](https://github.com/piraeusdatastore/piraeus-ha-controller)) — separate Kubernetes-aware controller. Watches Pod / Node / LINSTOR-resource state and does the cluster-aware actions: pod eviction on DRBD `StandAlone`, force-detach when a backing device fails, mount-node migration when the consumer Pod reschedules. It dials the LINSTOR REST API as a client. Verify against blockstor: end-to-end run with piraeus-ha-controller against our REST surface, document which endpoints it hits, fix any gaps. No code changes from blockstor expected unless an endpoint shape diverges from upstream; the audit needs to happen on the dev stand.
432432
- [x] **`linstor advise`** (2026-05-09): `GET /v1/view/advise/resources` and `GET /v1/resource-definitions/{rd}/advise` return per-RD recommendations (top-N pools by free capacity, sorted desc) without persisting anything. Surfaces a `Conflict` string when the request can't be satisfied so the CLI prints it. Tests: `TestAdviseRD`, `TestAdviseRDInsufficient`.
433433
- [x] **`linstor query-size-info` / spaceinfo** (2026-05-09): `POST /v1/resource-groups/{rg}/query-size-info` answers `max_vlm_size_in_kib = FreeCapacity of the n-th-largest pool` (n = place_count) — the cap that all replicas can fit at once, the value golinstor's pre-flight uses. `POST /v1/query-all-size-info` returns the per-RG map in one shot. EVICTED/LOST nodes excluded from capacity. Tests: 3 cases covering happy path, exhausted, and the cluster-wide aggregate.
434-
- [ ] **shared LUN provider (EXOS / SHARED) — deferred, not out-of-scope** (2026-05-09 reclassified). The SAN-shared-LUN model isn't needed today, but the architecture should leave room to slice volumes over a shared LUN later (e.g. multiple satellites carving an EXOS / NetApp / Ceph-RBD-as-shared-disk LUN). Upstream LINSTOR's accounting trick: a single "shared free-space" key keyed off the LUN identifier — multiple StoragePool CRDs on different nodes report the same shared key, the controller subtracts allocations once instead of summing per-node. Architectural hooks needed:
435-
- StoragePool gains an optional `SharedSpaceID` field (free-form key; empty = local pool)
436-
- the placer's free-capacity / query-size-info paths consult shared-space groups: pools sharing an ID contribute one free-capacity figure to the cluster total instead of summing
437-
- storage.Provider gets a notion of "this pool is backed by a LUN identified by X" so the satellite knows not to double-count
438-
- autoplace anti-affinity respects shared-space groups: don't place 2 replicas on pools that physically share the same backing LUN
439-
440-
Current behaviour stays as-is (501 on EXOS/REMOTE_SPDK kinds in pool create); that's a placeholder, not a design statement.
434+
- [x] **shared LUN provider (EXOS / SHARED) architectural hooks** (2026-05-09): `StoragePool` (CRD spec + REST shape) gains an optional `SharedSpaceID` field (empty = local pool). `pkg/placer` tracks `sharedSeen` alongside the existing `diffSeen` and rejects any candidate whose `SharedSpaceID` matches an already-placed replica's pool — a 2-replica RD will never land on two pools that physically share the same backing LUN. `pkg/rest/query_size_info.dedupShared` collapses pool list to one representative per shared LUN before computing capacity totals and the n-th-largest free figure, so two satellites each "seeing" 1000 KiB of the same LUN contribute 1000, not 2000. `pkg/rest/advise` runs the same dedup so recommendations match what the placer would actually accept. Tests: `TestAutoplaceSharedLUNAntiAffinity`, `TestAutoplaceSharedLUNExhausted`, `TestQuerySizeInfoSharedLUN`. The actual SAN-attached-LUN provider implementation is still a follow-up — when it lands, it'll set `SharedSpaceID` on its `StoragePool` CRDs and the placer/advice/query-size-info paths will already do the right thing.
441435

442436
### 8.6 Real-world testing
443437

@@ -461,18 +455,19 @@ The dev stand has been Talos+QEMU loopfile-backed. Production parity needs:
461455

462456
`tests/burnin-blockstor.sh` covers the 2-replica failover happy path. The remaining scenarios above each need a deterministic, automatable test in `tests/e2e/`:
463457

464-
- [ ] `tests/e2e/evacuate.sh` — node evacuate triggers replica migration
465-
- [ ] `tests/e2e/network-partition.sh` — partition + heal with no full re-sync
466-
- [ ] `tests/e2e/backing-device-fail.sh` — pull disk, expect graceful diskless-failover
467-
- [ ] `tests/e2e/snapshot-restore-cross-node.sh`
468-
- [ ] `tests/e2e/clone.sh`
469-
- [ ] `tests/e2e/resize-luks.sh`
470-
- [ ] `tests/e2e/resize-plain.sh`
471-
- [ ] `tests/e2e/two-primaries-live-migration.sh`
472-
- [ ] `tests/e2e/rwx-ganesha.sh`
473-
- [ ] `tests/e2e/two-volume-rd.sh`
474-
- [ ] `tests/e2e/tiebreaker.sh`
475-
- [ ] `tests/e2e/auto-diskful.sh`
458+
- [x] **e2e harness scaffolded** (2026-05-09): all 12 scenarios committed under `tests/e2e/`, plus a shared `lib.sh` (on_node, wait_uptodate, write_random, read_md5, delete_rd, require_workers, rd_apply). `make e2e NAME=<cluster> SCENARIO=<name>` invokes one; `make e2e-list` enumerates them. Each script captures the scenario's expected state transitions with explicit FAIL exits — running them on the dev stand validates that the corresponding controller/satellite path actually exercises end-to-end. Stand-side execution (and any failures the scenarios surface) tracked in 8.6 / 8.7 follow-up.
459+
- `tests/e2e/evacuate.sh` — node evacuate triggers replica migration
460+
- `tests/e2e/network-partition.sh` — partition + heal with no full re-sync
461+
- `tests/e2e/backing-device-fail.sh` — pull disk, expect graceful diskless-failover
462+
- `tests/e2e/snapshot-restore-cross-node.sh`
463+
- `tests/e2e/clone.sh`
464+
- `tests/e2e/resize-luks.sh`
465+
- `tests/e2e/resize-plain.sh`
466+
- `tests/e2e/two-primaries-live-migration.sh`
467+
- `tests/e2e/rwx-ganesha.sh`
468+
- `tests/e2e/two-volume-rd.sh`
469+
- `tests/e2e/tiebreaker.sh`
470+
- `tests/e2e/auto-diskful.sh`
476471

477472
**Exit criteria for Phase 8**: every checkbox above either lands or is moved to a separately-tracked "explicit out-of-scope" with rationale. Until then "production-ready" is overstating it; what we have is a CSI-compatible REST front-end with a verified happy path.
478473

stand/Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ KUBECONFIG := $(WORK_DIR)/kubeconfig
1313
export TALOSCONFIG
1414
export KUBECONFIG
1515

16-
.PHONY: stand-help up down reset piraeus oracle smoke smoke-blockstor burnin-blockstor use list stand-clean
16+
.PHONY: stand-help up down reset piraeus oracle smoke smoke-blockstor burnin-blockstor e2e e2e-list use list stand-clean
1717

1818
stand-help: ## list dev-stand targets
1919
@echo "stand targets (use NAME=<cluster>):"
@@ -43,6 +43,13 @@ smoke-blockstor: ## blockstor end-to-end smoke (RD → 2 replicas → bytes-perf
4343
burnin-blockstor: ## continuous PVC churn for DURATION seconds (default 86400 = 24h) (NAME=<n>, DURATION=<sec>)
4444
@./tests/burnin-blockstor.sh "$(WORK_DIR)" "$${DURATION:-86400}"
4545

46+
e2e: ## run a single e2e scenario (NAME=<cluster>, SCENARIO=<basename without .sh>)
47+
@test -n "$${SCENARIO}" || (echo "usage: make e2e NAME=<cluster> SCENARIO=<name>"; exit 2)
48+
@./tests/e2e/$${SCENARIO}.sh "$(WORK_DIR)"
49+
50+
e2e-list: ## list available e2e scenarios
51+
@ls tests/e2e/*.sh | xargs -n1 basename | grep -v '^lib\.sh$$' | sed 's/\.sh$$//'
52+
4653
use: ## print TALOSCONFIG/KUBECONFIG exports for shell eval
4754
@echo "export TALOSCONFIG=$(abspath $(TALOSCONFIG))"
4855
@echo "export KUBECONFIG=$(abspath $(KUBECONFIG))"

tests/e2e/auto-diskful.sh

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env bash
2+
#
3+
# usage: auto-diskful.sh WORK_DIR
4+
#
5+
# Tests Phase 8.3 "auto-diskful" promotion.
6+
# Setup:
7+
# - 2 diskful replicas of an RD on workers 1+2
8+
# - 1 DISKLESS replica on worker-3 (cluster-wide attachable pattern)
9+
# Steps:
10+
# 1. promote $N3 to Primary so Status.InUse=true on the DISKLESS replica
11+
# 2. ResourceReconciler sees: InUse + DISKLESS + non-tiebreaker + pool available
12+
# 3. removes DISKLESS flag, stamps StorPoolName onto Spec.Props
13+
# 4. satellite reconciler picks up the change, creates the LV / drbdadm attach
14+
15+
set -euo pipefail
16+
17+
WORK_DIR=${1:?work_dir required}
18+
export KUBECONFIG="$WORK_DIR/kubeconfig"
19+
20+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
21+
# shellcheck source=lib.sh
22+
source "$SCRIPT_DIR/lib.sh"
23+
24+
require_workers 3
25+
26+
RD=e2e-auto-diskful
27+
N1=test-worker-1
28+
N2=test-worker-2
29+
N3=test-worker-3
30+
31+
trap 'delete_rd "$RD"' EXIT
32+
33+
echo ">> apply 2 diskful + 1 DISKLESS"
34+
cat <<EOF | kubectl apply -f -
35+
apiVersion: blockstor.io.blockstor.io/v1alpha1
36+
kind: ResourceDefinition
37+
metadata: {name: ${RD}}
38+
spec:
39+
volumeDefinitions:
40+
- {volumeNumber: 0, sizeKib: 65536}
41+
EOF
42+
for n in "$N1" "$N2"; do
43+
cat <<EOF | kubectl apply -f -
44+
apiVersion: blockstor.io.blockstor.io/v1alpha1
45+
kind: Resource
46+
metadata: {name: ${RD}.${n}}
47+
spec:
48+
resourceDefinitionName: ${RD}
49+
nodeName: ${n}
50+
props: {StorPoolName: stand}
51+
EOF
52+
done
53+
54+
# Make sure $N3 is NOT a tiebreaker — explicitly diskless without the
55+
# TIE_BREAKER flag. (ResourceDefinitionReconciler would otherwise add
56+
# its own witness on $N3 for the 2-diskful → +tiebreaker rule, and it
57+
# would be exempt from auto-diskful.)
58+
cat <<EOF | kubectl apply -f -
59+
apiVersion: blockstor.io.blockstor.io/v1alpha1
60+
kind: Resource
61+
metadata: {name: ${RD}.${N3}}
62+
spec:
63+
resourceDefinitionName: ${RD}
64+
nodeName: ${N3}
65+
flags: ["DISKLESS"]
66+
EOF
67+
68+
wait_uptodate "$RD" "$N1" "$N2"
69+
70+
# Drive InUse on $N3 by promoting it.
71+
on_node "$N3" drbdadm primary "$RD"
72+
73+
echo ">> wait 30s for auto-diskful promotion"
74+
deadline=$(( $(date +%s) + 30 ))
75+
while (( $(date +%s) < deadline )); do
76+
flags=$(kubectl get "resources.blockstor.io.blockstor.io/${RD}.${N3}" \
77+
-o jsonpath='{.spec.flags}' 2>/dev/null || true)
78+
if [[ "$flags" != *"DISKLESS"* ]]; then
79+
break
80+
fi
81+
sleep 2
82+
done
83+
84+
if [[ "$flags" == *"DISKLESS"* ]]; then
85+
echo "FAIL: auto-diskful did not run (flags still: $flags)"
86+
exit 1
87+
fi
88+
89+
stor=$(kubectl get "resources.blockstor.io.blockstor.io/${RD}.${N3}" \
90+
-o jsonpath='{.spec.props.StorPoolName}')
91+
if [[ -z "$stor" ]]; then
92+
echo "FAIL: StorPoolName not stamped on promoted replica"
93+
exit 1
94+
fi
95+
96+
echo ">> AUTO-DISKFUL OK ($N3 promoted: pool=$stor, DISKLESS removed)"

tests/e2e/backing-device-fail.sh

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env bash
2+
#
3+
# usage: backing-device-fail.sh WORK_DIR
4+
#
5+
# Tests Phase 8.2 "events2 observer auto-detach on disk:Failed".
6+
# Setup:
7+
# - 2-replica RD, both UpToDate
8+
# - kill the loop file on Primary so DRBD's lower disk goes Failed
9+
# Expected:
10+
# - events2 observer sees disk:Failed
11+
# - satellite runs `drbdadm detach --force` on the Failed replica
12+
# - Primary stays Primary via the network leg (peer remains UpToDate)
13+
# - I/O continues without errors
14+
15+
set -euo pipefail
16+
17+
WORK_DIR=${1:?work_dir required}
18+
export KUBECONFIG="$WORK_DIR/kubeconfig"
19+
20+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
21+
# shellcheck source=lib.sh
22+
source "$SCRIPT_DIR/lib.sh"
23+
24+
require_workers 2
25+
26+
RD=e2e-disk-fail
27+
PRIMARY=test-worker-1
28+
PEER=test-worker-2
29+
30+
trap 'delete_rd "$RD"' EXIT
31+
32+
echo ">> apply 2-replica RD"
33+
rd_apply "$RD" "$PRIMARY" "$PEER"
34+
wait_uptodate "$RD" "$PRIMARY" "$PEER"
35+
36+
DEV=$(device_for_rd "$RD" "$PRIMARY")
37+
38+
echo ">> Primary write 256 KiB before fault"
39+
md5_pre=$(write_random "$PRIMARY" "$DEV" 262144)
40+
41+
echo ">> simulate backing-device failure on $PRIMARY"
42+
# loopfile pool puts each volume's image at /var/lib/blockstor-pool/<rd>_NNNNN.img
43+
# Truncating to 0 + losetup detach makes DRBD see Failed I/O.
44+
on_node "$PRIMARY" bash -c "
45+
losetup -j /var/lib/blockstor-pool/${RD}_00000.img | head -1 | cut -d: -f1 | xargs -r losetup -d
46+
"
47+
48+
echo ">> wait 30s for events2 observer to detach"
49+
sleep 30
50+
51+
# Detach success criteria: local disk in {Diskless, Failed}; peer still UpToDate.
52+
prim_disk=$(on_node "$PRIMARY" drbdsetup status "$RD" 2>/dev/null | grep "disk:" | head -1 || true)
53+
if [[ "$prim_disk" != *"Diskless"* && "$prim_disk" != *"Failed"* ]]; then
54+
echo "FAIL: $PRIMARY disk did not transition (got: $prim_disk)"
55+
exit 1
56+
fi
57+
58+
peer_disk=$(on_node "$PEER" drbdsetup status "$RD" 2>/dev/null | grep "disk:" | head -1 || true)
59+
if [[ "$peer_disk" != *"UpToDate"* ]]; then
60+
echo "FAIL: $PEER disk no longer UpToDate (got: $peer_disk)"
61+
exit 1
62+
fi
63+
64+
echo ">> read on $PRIMARY (network-leg) — md5 must match $md5_pre"
65+
md5_after=$(read_md5 "$PRIMARY" "$DEV" 262144)
66+
if [[ "$md5_after" != "$md5_pre" ]]; then
67+
echo "FAIL: post-detach read returned different data"
68+
exit 1
69+
fi
70+
71+
echo ">> BACKING-DEVICE-FAIL OK (observer detached, peer covers reads via network)"

tests/e2e/clone.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
#
3+
# usage: clone.sh WORK_DIR
4+
#
5+
# Tests CSI clone (volume-from-volume, no intermediate VolumeSnapshot).
6+
# Same plumbing as snapshot-restore but driven through the CSI shape:
7+
# the CSI controller-publish-volume path takes a VolumeContentSource of
8+
# kind Volume → blockstor's snapshot-restore-resource is called by the
9+
# CSI shim with an auto-generated transient snapshot name.
10+
#
11+
# Setup:
12+
# - source RD with known data on $N1 + $N2
13+
# - call /v1/resource-definitions/{rd}/clone with a target name
14+
# Expected:
15+
# - target RD exists
16+
# - target's data == source's data (md5 match)
17+
18+
set -euo pipefail
19+
20+
WORK_DIR=${1:?work_dir required}
21+
export KUBECONFIG="$WORK_DIR/kubeconfig"
22+
23+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
24+
# shellcheck source=lib.sh
25+
source "$SCRIPT_DIR/lib.sh"
26+
27+
require_workers 2
28+
29+
RD_SRC=e2e-clone-src
30+
RD_DST=e2e-clone-dst
31+
N1=test-worker-1
32+
N2=test-worker-2
33+
34+
trap 'delete_rd "$RD_SRC"; delete_rd "$RD_DST"' EXIT
35+
36+
echo ">> apply source RD"
37+
rd_apply "$RD_SRC" "$N1" "$N2"
38+
wait_uptodate "$RD_SRC" "$N1" "$N2"
39+
40+
DEV=$(device_for_rd "$RD_SRC" "$N1")
41+
RD=$RD_SRC
42+
md5_src=$(write_random "$N1" "$DEV" 262144)
43+
44+
# blockstor's clone is implemented as snapshot-restore under the hood.
45+
# A direct CSI clone path (CreateVolume with VolumeContentSource of kind
46+
# Volume) compiles to: take a transient snapshot of the source, restore
47+
# into the target, drop the snapshot. We reproduce that here.
48+
SNAP=clone-$(date +%s)
49+
50+
echo ">> internal: take transient snapshot $SNAP for clone"
51+
kubectl -n "$NS" exec deploy/blockstor-controller -- \
52+
curl -fsS -XPOST -H'Content-Type: application/json' \
53+
"http://localhost:3370/v1/resource-definitions/${RD_SRC}/snapshots" \
54+
-d "{\"name\":\"${SNAP}\",\"nodes\":[\"${N1}\",\"${N2}\"]}"
55+
56+
echo ">> clone $RD_SRC$RD_DST"
57+
kubectl -n "$NS" exec deploy/blockstor-controller -- \
58+
curl -fsS -XPOST -H'Content-Type: application/json' \
59+
"http://localhost:3370/v1/resource-definitions/${RD_SRC}/snapshot-restore-resource" \
60+
-d "{\"to_resource\":\"${RD_DST}\",\"snapshot_name\":\"${SNAP}\"}"
61+
62+
kubectl -n "$NS" exec deploy/blockstor-controller -- \
63+
curl -fsS -XPOST -H'Content-Type: application/json' \
64+
"http://localhost:3370/v1/resource-definitions/${RD_DST}/autoplace" \
65+
-d "{\"select_filter\":{\"place_count\":2,\"storage_pool\":\"stand\"}}"
66+
67+
RD=$RD_DST
68+
wait_uptodate "$RD_DST" "$N1" "$N2"
69+
70+
DEV_DST=$(device_for_rd "$RD_DST" "$N1")
71+
md5_dst=$(read_md5 "$N1" "$DEV_DST" 262144)
72+
73+
if [[ "$md5_src" != "$md5_dst" ]]; then
74+
echo "FAIL: clone data differs (src=$md5_src dst=$md5_dst)"
75+
exit 1
76+
fi
77+
78+
echo ">> CLONE OK"

tests/e2e/evacuate.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
#
3+
# usage: evacuate.sh WORK_DIR
4+
#
5+
# Tests Phase 8.3 "linstor node evacuate actually migrates replicas".
6+
# Setup:
7+
# - 3-node cluster, 2-replica RD on workers 1+2, worker-3 idle
8+
# - flag worker-1 EVICTED via REST
9+
# Expected:
10+
# - NodeReconciler creates a 3rd Resource on worker-3 (the migration)
11+
# - parent RD now has 3 Resources
12+
# - worker-1's Resource still present (operator decides when to drop it)
13+
14+
set -euo pipefail
15+
16+
WORK_DIR=${1:?work_dir required}
17+
export KUBECONFIG="$WORK_DIR/kubeconfig"
18+
19+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
20+
# shellcheck source=lib.sh
21+
source "$SCRIPT_DIR/lib.sh"
22+
23+
require_workers 3
24+
25+
RD=e2e-evacuate
26+
SOURCE=test-worker-1
27+
PEER=test-worker-2
28+
TARGET=test-worker-3
29+
30+
trap 'delete_rd "$RD"' EXIT
31+
32+
echo ">> apply 2-replica RD ($SOURCE + $PEER)"
33+
rd_apply "$RD" "$SOURCE" "$PEER"
34+
wait_uptodate "$RD" "$SOURCE" "$PEER"
35+
36+
echo ">> mark $SOURCE EVICTED"
37+
kubectl patch nodes.blockstor.io.blockstor.io "$SOURCE" --type=merge \
38+
-p '{"spec":{"flags":["EVICTED"]}}'
39+
40+
echo ">> wait up to 60s for migration to $TARGET"
41+
deadline=$(( $(date +%s) + 60 ))
42+
while (( $(date +%s) < deadline )); do
43+
if kubectl get "resources.blockstor.io.blockstor.io/${RD}.${TARGET}" >/dev/null 2>&1; then
44+
break
45+
fi
46+
sleep 2
47+
done
48+
49+
if ! kubectl get "resources.blockstor.io.blockstor.io/${RD}.${TARGET}" >/dev/null 2>&1; then
50+
echo "FAIL: NodeReconciler did not create replacement on $TARGET"
51+
kubectl get resources.blockstor.io.blockstor.io --no-headers | awk -v rd="$RD" '$1 ~ rd'
52+
exit 1
53+
fi
54+
55+
# Source replica must STILL exist — EVICTED is a "drain" semantic, not
56+
# a "delete". LOST flag is the destructive variant (covered separately
57+
# by the eviction unit tests).
58+
if ! kubectl get "resources.blockstor.io.blockstor.io/${RD}.${SOURCE}" >/dev/null 2>&1; then
59+
echo "FAIL: source replica on $SOURCE removed prematurely (EVICTED ≠ LOST)"
60+
exit 1
61+
fi
62+
63+
echo ">> EVACUATE OK ($SOURCE+$PEER → migrated to $TARGET, source still present)"

0 commit comments

Comments
 (0)