Skip to content

feat: Kubernetes-native VolumeGroupSnapshot on consistency groups (P0-4) - #504

Merged
geoffrey1330 merged 32 commits into
mainfrom
support_volume_group_snapshot_consistenct_group
Sep 16, 2026
Merged

geoffrey1330 merged 32 commits into
mainfrom
support_volume_group_snapshot_consistenct_group

Conversation

@geoffrey1330

@geoffrey1330 geoffrey1330 commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Kubernetes-native VolumeGroupSnapshot on consistency groups

Summary

This PR wires simplyblock's standalone consistency groups into the Kubernetes
VolumeGroupSnapshot API (groupsnapshot.storage.k8s.io/v1beta1): PVCs labeled
storage.simplyblock.io/consistency-group join one placement-pinned backend group at
provisioning time, and a VolumeGroupSnapshot over them materializes as one
crash-consistent backend generation with one member VolumeSnapshot per PVC, restorable
through the ordinary per-member snapshot path.

The design document and test plan ship in the PR
(operator/docs/designs/design-consistency-groups.md,
operator/docs/tests/test-plan-consistency-groups.md).

What's in this PR

CSI driver

  • Group provisioning (§4.1): the provisioner forwards the PVC's
    storage.simplyblock.io/consistency-group label as consistency_group in the
    volume-create body, so the control plane joins the volume to the named group and pins
    it to the group's node/LVS before placement runs.
  • Group-forming restore (§7.2): the label is forwarded on the CLONE body too, so
    restore PVCs carrying the label birth a new placement-pinned group from the clones.
  • GroupController service (§9): CreateVolumeGroupSnapshot resolves the group from
    the source volume handles, verifies the requested set equals the group's current
    membership (backstop for the admission window), and takes one generation.
    GetVolumeGroupSnapshot and DeleteVolumeGroupSnapshot operate on the
    {cluster}:{pool}:{group}:{seq} group-snapshot id; a missing group or generation
    deletes as success (§9.3).
  • Identity: advertises GROUP_CONTROLLER_SERVICE.

Operator

  • VolumeGroupSnapshotOps (§7.4, new kind): a one-shot, Job-analogous operation
    with action: Restore — one PersistentVolumeClaim per member snapshot of the
    target's generation, named <namePrefix>-<source PVC>, waited to bind, with typed
    phase/step status, CEL-immutable spec, and an admission webhook that rejects an
    unresolvable volumeGroupSnapshotRef at apply. restore.consistencyGroup labels the
    clones so they form a new group; restore.enablePartialRestore opts into restoring
    an incomplete generation, which otherwise fails naming the missing members.
  • VolumeGroupSnapshot admission webhook (§9.4): rejects at apply a selector that
    does not resolve to exactly one group's current membership.
  • VolumeMigration admission webhook (§9.5): refuses migrating a PV that is a
    consistency-group member; the placement pin is load-bearing for crash consistency.
  • Operator-owned CSI deployment: the CSIVolumeGroupSnapshot feature gate on the
    csi-snapshotter sidecar and the groupsnapshot.storage.k8s.io RBAC live in the
    SimplyblockDriver reconciler (the chart no longer templates the CSI workloads).

Helm chart

  • VolumeGroupSnapshot CRDs (v1beta1), the snapshot-controller with the
    CSIVolumeGroupSnapshot feature gate, the webhook manifests, and the new
    VolumeGroupSnapshotOps CRD. A SimplyblockDriver CR is seeded in
    operator_customresources.yaml (its name derives the adopted workload names).
  • No default VolumeGroupSnapshotClass ships: classes are the user's to author
    (the regression script creates its own).
  • RBAC fix: the snapshot-controller ClusterRole grants create on
    volumesnapshots (matching upstream external-snapshotter v8.2.0) — the group flow
    is the one place the controller creates member VolumeSnapshot objects itself.

Companion control-plane PR (sbcli cg-standalone-impl)

The backend half: standalone ConsistencyGroup model, join-at-create and join-at-clone
with placement pinning, the group-snapshot generation endpoints, and the sbctl cg
commands — plus the fixes this PR's live validation surfaced: group records written
outside their FDB keyspace, a controller import cycle, the ns_id default leaking into
clone placement, detached members of a never-snapshotted group counted forever, and
controller refusals surfacing as opaque 500s instead of a 422 with the reason.

Validation

Go tests: GroupController create/get/delete against a mock control plane, admission
tables for all three webhooks, label forwarding on create AND clone, and the
VolumeGroupSnapshotOps controller (claim derivation, group labels, partial-restore
gate, collision handling, readiness waiting, bind-to-Succeeded, terminal inertness).

Live cluster, regression_test/20/test_volume_group_snapshot.sh:

# Case
0 Preflight: CRDs and feature gate present; script-owned class created
1 Labeled PVCs form ONE placement-pinned backend group (one node, one group_id)
2 VolumeGroupSnapshot goes ReadyToUse; content carries the handle; one member VolumeSnapshot per PVC
3 Crash consistency under load: restore of one generation satisfies the prefix property (spread <= 1, hashes valid)
4 Selector matching an unlabeled PVC rejected at apply (§9.4 webhook)
5 VolumeMigration of a member rejected at apply (§9.5 webhook)
6 Generations increment and delete independently; the group survives generation deletion
7 Late joiner (new labeled volume) lands on the pin and appears in the NEXT generation
8 Deleting a member closes its epoch; its snapshot in the earlier generation stays ReadyToUse
9 A restore from a member snapshot is NOT a group member
10 One applied VolumeGroupSnapshotOps restores a generation (bound claim per member); unresolvable ref rejected at apply
11 restore.consistencyGroup forms a new pinned backend group from the clones
12 N groups x M members: every group forms, pins, snapshots as a distinct generation (selected by the consistency-group label itself), and restores via its own ops
13 Documented limits driven through the group, with data at every step: CHAIN_MAX (100) generations grow every member's chain; generation 101 refused with a reason; a mid-chain member restore holds exactly its point in time; CLONE_MAX (500) clones of one member snapshot all bind, clone 501 refused with a reason
14 Large-group timings under copy-on-write: the 20-member cap at 100/200/300Gi, filled with incompressible data (BIG_FILL_PCT of each member's size), a continuous random-overwrite workload across every member, generations taken BIG_SNAP_INTERVAL (30s) apart — reporting ReadyToUse latency, the max single-I/O stall (the LVS freeze window), and the CoW churn the timings were measured against
15 Crash consistency under dependent writes: one writer, strictly serialized checksummed 4K writes round-robin across 20 members (O_DIRECT|O_DSYNC, depth 1); 10 mid-workload generations plus a back-to-back pair each verified as a contiguous prefix {0..M} (present + superseded = M + 1); staggered per-volume snapshots flagged VIOLATION as the negative control
16 Dynamic membership (design §4.5, Phase 4): labeling an existing bound PVC joins its volume to the group and the next generation contains it; removing the label detaches it (epoch closed one-way, its earlier snapshot stays ReadyToUse); re-adding the label is refused with ConsistencyGroupJoinRefused on the PVC

Notes and limitations

  • The group-snapshot API is v1beta1 and gated behind CSIVolumeGroupSnapshot; the
    chart enables both.
  • Group membership is capped at 20 members (documented in the design): the whole set
    freezes for one bdev_lvol_snapshot_group call.
  • Known open item on the control-plane side: ensure_group's read-then-create is not
    yet transactional, so a burst of first-member creates against multiple API replicas
    can still race duplicate groups; tracked for a follow-up.

Test results

user@users-MacBook-Pro-2 ~ % kubectl -n simplyblock get volumegroupsnapshot
NAME       READYTOUSE   VOLUMEGROUPSNAPSHOTCLASS             VOLUMEGROUPSNAPSHOTCONTENT                              CREATIONTIME   AGE
vgs-gen1   true         simplyblock-csi-groupsnapshotclass   groupsnapcontent-33891814-6615-46af-b1cb-b2ab8ff00c00   18s            19s
user@users-MacBook-Pro-2 ~ % kubectl -n simplyblock get volumesnapshots
NAME                   READYTOUSE   SOURCEPVC   RESTORESIZE   SNAPSHOTCONTENT           CREATIONTIME   AGE
snapshot-0f0eda8e...   true         vgs-pvc-3   2Gi           snapcontent-0f0eda8e...   12s            10s
snapshot-1892710b...   true         vgs-pvc-2   2Gi           snapcontent-1892710b...   12s            11s
snapshot-7b4a2fa9...   true         vgs-pvc-1   2Gi           snapcontent-7b4a2fa9...   12s            8s
user@users-MacBook-Pro-2 ~ % kubectl -n simplyblock get pvc
NAME            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
vgs-pvc-1       Bound    pvc-15f94835-fe3a-4915-b0a1-475d493642a5   2Gi        RWO            test-vgs-sc    2m3s
vgs-pvc-2       Bound    pvc-d72dfd92-5346-4f43-a4a6-e78bd7c7bcc5   2Gi        RWO            test-vgs-sc    2m1s
vgs-pvc-3       Bound    pvc-2c403207-9476-495c-bb6b-25a59e5282c8   2Gi        RWO            test-vgs-sc    119s
vgs-restore-1   Bound    pvc-a63e1413-4ef1-471d-89d3-436d1187e9e9   2Gi        RWO            test-vgs-sc    12s
vgs-restore-2   Bound    pvc-d77d4dce-926a-4b3c-a69d-92d663d4c77d   2Gi        RWO            test-vgs-sc    10s
vgs-restore-3   Bound    pvc-c86b7bc5-6060-4e2f-9972-9aa89b4a44c4   2Gi        RWO            test-vgs-sc    8s
user@users-MacBook-Pro-2 ~ % kubectl -n simplyblock get volumegroupsnapshotops        
NAME        GROUPSNAPSHOT   ACTION    PHASE       STEP             BOUND   MESSAGE                AGE
vgs-ops-1   vgs-gen-ops     Restore   Succeeded   WaitingForBind   4       restored 4 member(s)   47s
vgs-ops-2   vgs-gen-ops     Restore   Succeeded   WaitingForBind   4       restored 4 member(s)   18s
Events:
  Type     Reason                Age                From                                                                                   Message
  ----     ------                ----               ----                                                                                   -------
  Normal   ExternalProvisioning  14s (x2 over 14s)  persistentvolume-controller                                                            Waiting for a volume to be created either by the external provisioner 'csi.simplyblock.io' or manually by the system administrator. If volume creation is delayed, please verify that the provisioner is running and correctly registered.
  Normal   Provisioning          8s (x3 over 14s)   csi.simplyblock.io_vm01.simplyblock3.localdomain_9ac1932e-c847-4b00-bebd-0f527a89262e  External provisioner is provisioning volume for claim "simplyblock/vgs-pvc-21"
  Warning  ProvisioningFailed    8s (x3 over 14s)   csi.simplyblock.io_vm01.simplyblock3.localdomain_9ac1932e-c847-4b00-bebd-0f527a89262e  failed to provision volume with StorageClass "test-vgs-sc": rpc error: code = InvalidArgument desc = POST 422: consistency group vgs-group already has the maximum 20 members

@noctarius
noctarius force-pushed the main branch 2 times, most recently from 60dceb7 to fbaabe4 Compare September 9, 2026 10:21
geoffrey1330 and others added 13 commits September 9, 2026 14:08
…oups and VolumeGroupSnapshot, drop replication
…-time membership check, health precheck, migration exclusion, restore open question
Implement the CSI GroupController so a VolumeGroupSnapshot snapshots an
existing, placement-pinned consistency group:

- controlplane: cluster-scoped client calls to resolve a volume's group,
  read live membership, and take/get/delete a snapshot generation.
- groupsnapshot.go: Create verifies the source handles equal the group's
  current membership (FAILED_PRECONDITION otherwise, §9.2) then takes one
  generation; Delete/Get resolve the group from a self-contained group
  snapshot id ({cluster}:{pool}:{groupUUID}:{seq}); a missing group or
  generation is delete success (§9.3).
- register the GroupController service when the controller implements it;
  advertise GROUP_CONTROLLER_SERVICE only when EnableGroupController is set,
  so the sanity harness (arbitrary-volume group tests, incompatible with the
  persistent-group model, §3/§11.6) is not offered them.
- tests: membership-equality (equal/extra/missing/non-member/two-group),
  delete-missing-success, get round-trip, backend-error surfaced, capability
  gating. csi-test, csi-lint, and house-style all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Migration (design §9.4, §9.5)

Two validating webhooks, admission-only (no reconcile):

- VolumeGroupSnapshot (§9.4): label check fail-closed (every selected PVC
  shares one non-empty storage.simplyblock.io/consistency-group value) and
  membership check fail-open (resolve the group by name, compare the selected
  lvols to its current members, admit on a backend blip). Rejects a selector
  that spans groups, matches an unlabeled PVC, matches nothing, or does not
  equal the group's membership, at kubectl apply instead of at snapshot time.
- VolumeMigration (§9.5): resolve the target PV to its backing lvol and refuse
  the create when the volume, or any sibling sharing its NVMe subsystem, is a
  consistency-group member (pinned placement, §8.4). Fail-open; the backend
  refusal is the backstop.
- webapi: GetConsistencyGroupByName, GetConsistencyGroupMembers, and group_id
  on VolumeInfo. Add the external-snapshotter v8 client for the VGS type;
  register both webhooks in main.go; manifests + helm-sync regenerated.
- tests: U-12..U-19 (VGS), U-21..U-24 (migration). operator-test, operator-lint,
  and house-style green (pre-existing rebalancing.go debt outside the diff aside).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ign §9)

Turn on the Kubernetes group-snapshot path the GroupController and webhooks
already implement:

- ship the external-snapshotter v8.2.0 VolumeGroupSnapshot,
  VolumeGroupSnapshotContent, and VolumeGroupSnapshotClass CRDs (guarded and
  resource-policy: keep, like the per-volume snapshot CRDs).
- enable the CSIVolumeGroupSnapshot feature gate on the csi-snapshotter
  sidecar and the snapshot-controller (both already v8.2.0).
- add a default VolumeGroupSnapshotClass for csi.simplyblock.io.
- grant the csi-snapshotter sidecar SA the volumegroupsnapshotcontents /
  classes RBAC (the snapshot-controller RBAC already had the group rules).

helm lint + template render clean (114 docs, 6 CRDs, the class, both feature
gates, sidecar RBAC); house-style green; helm-sync leaves these chart-native
files untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
design §4.1 states the cap and its rationale (bounded freeze window), §12
adds the over-cap failure mode, and the test-plan group-size axis notes the
cap boundary. Matches MAX_CONSISTENCY_GROUP_MEMBERS in sbcli.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The provisioner never sent consistency_group, so labeled PVCs were created
as ungrouped volumes: group_id stayed empty and CreateVolumeGroupSnapshot
rejected them with FAILED_PRECONDITION 'not a consistency-group member'
(design §4.1, P0-3 — the missing provisioner half of the group-join path).

- CreateLVolData carries consistency_group (omitempty).
- fetchPVCAnnotations becomes fetchPVCMeta, returning labels too from the one
  Get; prepareCreateVolumeReq reads storage.simplyblock.io/consistency-group
  and forwards it.
- tests: a labeled PVC forwards the group; an unlabeled one omits it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@geoffrey1330 geoffrey1330 changed the title docs: add consistency-groups design and test plan feat: Kubernetes-native VolumeGroupSnapshot on consistency groups (P0-4) Sep 11, 2026
@geoffrey1330
geoffrey1330 requested review from boddumanohar and noctarius and removed request for noctarius September 14, 2026 09:07
Comment thread csi-driver/internal/csi/controller/groupsnapshot.go Outdated
Comment thread operator/internal/webhook/volumehandle.go
Comment thread operator/internal/webhook/volumegroupsnapshot_validator.go Outdated
Comment thread operator/api/v1alpha1/volumegroupsnapshotops_types.go Outdated
Comment thread operator/config/webhook/manifests.yaml Outdated
@geoffrey1330
geoffrey1330 merged commit 9303f49 into main Sep 16, 2026
25 checks passed
@geoffrey1330
geoffrey1330 deleted the support_volume_group_snapshot_consistenct_group branch September 16, 2026 15:14
noctarius added a commit that referenced this pull request Sep 17, 2026
Brings the VolumeGroupSnapshot work (#504), lblk device support (#437), the
client-side compression design (#398), and two openapi.json syncs (#541, #549)
onto the branch carrying the CRD redesign.

Thirteen files conflicted. The eight generated ones — the deepcopy, the
StorageNode CRD in its four copies, the manager role, the chart's webhook
template, and install.yaml — were resolved by regenerating rather than by
editing, since a generated file has no side to prefer.

Three were modify-and-delete, where the lblk work edited the v1alpha1
controllers this branch retired: storagenode_controller.go, the per-node
ConfigMap writer, and the StorageNodeSet unit test. The deletions stand. Their
replacements under internal/controllers/node are what the operator runs, and
restoring a v1alpha1 controller to hold a feature would reinstate the model the
redesign removed.

cmd/main.go took both import sides.

One test came across orphaned. TestBuildStorageNodeSetDaemonSet_ConfigGenerator-
MountsDevAndSys guards the /dev and /sys mounts node_configure.py's lblk
eligibility check needs, and it was written against the v1alpha1 builder; it is
rewritten against BuildStorageNodeDaemonSet, which is the builder this operator
runs and which carries those mounts. The assertion is about the container rather
than about which kind describes it.

lblk arrives half-wired, and this commit does not finish it. What works is the
cluster half and the workload half: StorageCluster.spec.deviceClass still
resolves to sbcli's device_mode, and the DaemonSet's init script still assembles
--lblk, --blk-names, --blk-names-exclude, --blk-serials, --jm-percent, and
--force-format. What is missing is the field half. The six selectors exist on
v1alpha1's StorageNodeSet and StorageNode, which this operator no longer reads,
and have no counterpart on v1alpha2; the per-node ConfigMap writer under
internal/controllers/node therefore emits none of the LBLK or BLK_ keys the init
script reads, so every flag resolves empty. Selecting devices by lblk is inert
on this branch until v1alpha2 carries the fields, which is its own change rather
than a conflict resolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants