fix(rest): unbreak CSI clone-from-volume and make snapshot restore idempotent - #190
Open
Andrei Kvapil (kvaps) wants to merge 30 commits into
Open
Andrei Kvapil (kvaps) wants to merge 30 commits into
Andrei Kvapil (kvaps) wants to merge 30 commits into
Conversation
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent CSI-facing defects reported from a Cozystack stand running the blockstor backend.
Clone rejected every request golinstor sends
The endpoint declared five fields and decodes with
DisallowUnknownFields, solayer_list, which linstor-csi defaults to[DRBD, STORAGE]and never omits, was a 400 before any of the handler ran. Every clone-from-volume failed whatever the StorageClass said, and on Cozystack the platform-widecloneStrategyOverride: csi-cloneroutes every disk clone through here, so aVMDiskwith asource.disksat inCSICloneInProgressindefinitely.resource_groupwas the next 400 waiting behind it, anddelete_namespacesthe one after that. It rides withoverride_propsanddelete_propson every props-modify body upstream sends, so declaring its two neighbours and not it left a clone body carrying it on the same refusal. The triple is now embedded from golinstor rather than respelled, since respelling is how it went missing.The two fields that carry meaning are honoured on both clone paths: the volume-less shortcut and the snapshot-restore path CSI actually takes. The stack is validated the way
rg modifyvalidates its own, asked for the LUKS prerequisite like every other writer of a layer stack, and the resource group is checked to exist: it lands on the target on both paths and went past no validator, so a typo produced a clone whose parent group does not exist. It lists fine and places badly, because the placer's Controller→RG→RD prop walk drops the RG tier without a word.external_nameandvolume_passphrasesare refused rather than accepted and dropped. Ignoring the first returns a definition under a name the caller did not ask for; ignoring the second materialises volumes with keys the caller does not hold. That is the failure modesrc_snap_nameis already refused for on this endpoint.layer_listis honoured with one refusal, and it is the reason to read this section twice: on a source that has volumes, the requested stack must be the source's, compared as a set, since order is the stack's own and case folds. The clone data plane restores the source's bytes and brings the layer stack up over them, in that order, and every layer's bring-up writes to the device it is handed:luks.Formattreats a device with no LUKS header as one to format, andcreate-mdruns with--forceovermeta-disk internal, stamping metadata across the tail of the same bytes. So a layer added here lands on the data the clone just restored and the clone reports COMPLETE over it, and a layer dropped leaves the target reading data the missing layer wrote. A source with no recorded stack means the upstream default[DRBD, STORAGE], resolved the way every other reader resolves it, because reading it as "no layers" would make linstor-csi's own request look like adding both and refuse every clone-from-volume. A volume-less source has no bytes to lose and still takes any stack.Snapshot restore was not idempotent
CSI requires
CreateVolumeto be idempotent, and external-provisioner has no other way to make progress after a partial failure. A restore that created the ResourceDefinition and then failed left it behind, so every retry answeredobject already exists, so the first partial failure was terminal for that volume name, the PVC stayedPending, and the leftover had to be deleted by hand.A repeat now resumes when the definition under that name carries this restore's own marker. The marker is stamped with the definition, before the volumes are hydrated and the replicas placed, so it says the restore STARTED and not that it finished, and answering success on it would trade a terminal failure for a silent incomplete one, with CSI seeing a ready volume nothing ever finishes. The clone path now draws the same split: it used to answer 201 "already cloned" on the marker alone, so the claim that it already did was wrong, and this PR is where it becomes true.
Three things stay refusals on both paths: a definition under that name this operation did not create; its own leftover carrying the DELETE flag, since finishing that one races the tear-down reaping what it writes; and a retry asking for a different shape than the attempt that created the leftover, because resuming keeps that leftover and the request's
resource_groupwould be validated and then dropped. A leftover internal snapshot that has fallen behind the source (a volume added, or resized) is refused rather than cloned from, since resuming over it materialises the clone at the old shape and reports it complete. And the marker comparison is the equality LINSTOR itself uses, in one place, on both doors: LINSTOR folds name case, so a retry arriving as--from-snapshot SNAPover a marker writtensnap, or under an uppercase clone target, read as somebody else's definition and was refused, which is the terminal-on-first-failure behaviour the resume path exists to end.A restore into a definition carrying the DELETE flag is refused on the volume-definition endpoint too, which fetched the target and threw it away.
This does not address whatever failed the first attempt, which the report could not pin down either. It makes that failure recoverable instead of terminal.
Testing
Every behavioural change here is pinned by a test that was checked by reverting the change and confirming the named test goes red, including the two directions of the LUKS refusal, the resumed clone (whose status is 201 either way, so only the volumes tell a resumed clone from one that merely agreed it had happened), the DELETE-flagged leftover on both paths, the case-folded retry, and the unknown resource group on both clone paths. The refusal tests carry positive controls, because a refusal test that passes for the wrong reason is the failure mode two of the earlier tests on this endpoint had.
The case-folding test needs a store that folds names the way the Kubernetes one does, which the in-memory store does not, so it runs through a decorator that models exactly that.
docs/cli-parity-known-deltas.mdgains rows 86 and 87 for the refusals and the retry semantics.golangci-lintis clean on the touched files; thepkg/restsuite passes apart from a pre-existingserver did not stop within 2s after cancelflake that reproduces unchanged on the merge base.Summary by CodeRabbit
New Features
Bug Fixes
Documentation