feat: add storage node drain-remove workflow#294
Conversation
|
I think we need a lot more test scenarios. Testing is not just about testing ones to ensure the basics work. We need more behavior tests on specific code segments and functions. Not only for us to ensure it works today, but also to ensure that stuff isn't breaking tomorrow by a thought-off unrelated change. Many should be possible as unit tests which just ensure that the code itself behaves correctly now, and fails to fail in the future (with code changes). Some may require integration tests which should be considered e2e tests (as far as possible) or laying out a test concept for @RaunakJalan.
|
I have added unit-test that covers the outlined scenarios |
I see that some off them are implemented as very very basic tests. Like for example that if pinned and unmanaged volumes are found. You added a unit tests to check that both are return from the validation function, but not they both are reconciled into events on the StorageNodeSet which tells the user about both issues. This is what I actually meant. Only the first check (which is We should validate everything and mention all at once to prevent the user makes circles. For all the above tests, it's good to test them as soon as possible, but please validate if there are high-up location where they would have impact and we need to ensure the user understands what's happening. Not sure how many really are affected but I saw this specific one since I thought about it specifically as feedback to the user. |
|
Also, what is with the open questions? Are those answered? If so, what are the answers and how are they respected in the design? I typically strike them out with a quick explanation when I answered one and reconcile the answer into the design itself to ensure consistency. |
…le events and status
noctarius
left a comment
There was a problem hiding this comment.
There are a few comments like enhanced error handling, a small helper extraction and one or two questions regarding sub-process fails (like a failing volume migration).
| } | ||
|
|
||
| // All existing CRs are completed — update counters, clean up and advance. | ||
| if inProgress == 0 && completed == len(vmigList.Items) { |
There was a problem hiding this comment.
What if a migration fails? Seems like there is no terminal operation for this and it would spin forever since inProgress would be >1 and completed not matching the migration list?
| body, status, err := apiClient.Do(ctx, http.MethodGet, endpoint, nil) | ||
|
|
||
| if action == "remove" && status == http.StatusNotFound { | ||
| if action == utils.NodeActionRemove && status == http.StatusNotFound { |
There was a problem hiding this comment.
Do we need to reconcile the status StorageNodeSet status field (removing the node) or does it happen automatically? Not sure if we should also update the workers property since it's not true anymore.
There was a problem hiding this comment.
status.nodes is refreshed automatically via syncTrackedNodesStatus. We intentionally leave spec.workerNodes unchanged. it's user-declared and the stale status.nodes entry also prevents reconcileWorkerNode from re-provisioning the removed node.
There was a problem hiding this comment.
This isn't great error handling. HTTP statuses above 3xx mean different things. Just blindly retrying is wrong here. If a node is already removed the control plane would most probably answer with 409 (conflict) or 404 (not found). A retry would not solve this issue. See the new error classification system in the CSI driver for hints which is retry-able or not (https://github.com/simplyblock/simplyblock-csi/blob/master/pkg/spdk/errorclass.go). I know this isn't directly part of this PR, but since we already hit this part, we should fix it.
I'm happy to extract the error classification already and make it available here, too.
There was a problem hiding this comment.
Sure, maybe i can extract and use it as part of my PR
There was a problem hiding this comment.
I've extracted the error classification into the operator errorclass.go
| return ctrl.Result{}, nil | ||
| } | ||
|
|
||
| // 5xx: transient backend error — retry without resuming the node. |
There was a problem hiding this comment.
Not every 5xx error is retry-able. See the other comment.
| } | ||
|
|
||
| // 4xx (other than 404): permanent backend rejection — resume and fail. | ||
| return r.resumeAndFail(ctx, apiClient, clusterUUID, snCR, |
There was a problem hiding this comment.
Not every 4xx is a permanent error, like 429 is too many requests.
| nodeUUID := snCR.Spec.NodeUUID | ||
|
|
||
| resumeEndpoint := fmt.Sprintf("/api/v2/clusters/%s/storage-nodes/%s/resume", clusterUUID, nodeUUID) | ||
| if _, _, err := apiClient.Do(ctx, http.MethodPost, resumeEndpoint, nil); err != nil { |
There was a problem hiding this comment.
Why is resume a best effort only?
There was a problem hiding this comment.
You're right, if resume fails the node stays suspended which is bad. Fixed — we now retry on transient errors and only give up on permanent ones.
| Namespace: pv.Spec.ClaimRef.Namespace, | ||
| Name: pv.Spec.ClaimRef.Name, | ||
| }, &pvc); err != nil { | ||
| log.Error(err, "matchVolumesToPVs: failed to get PVC, treating volume as unmanaged", |
There was a problem hiding this comment.
I think this is not a perfect solution but it's better than ignoring the error. Maybe it's worth adding to the functions documentation that a failed PV request / mapping will temporarily mark all volumes as unmanaged.
| } | ||
| } | ||
|
|
||
| re, reErr := regexp.Compile(filterRegex) |
There was a problem hiding this comment.
I think this should be precompiled at bootup. That way it will also fast fail when there is an error in the filter expression.
| clusterUUID string, | ||
| nodeUUID string, | ||
| ) ([]webapi.VolumeInfo, error) { | ||
| pools, err := apiClient.GetStoragePools(ctx, clusterUUID) |
There was a problem hiding this comment.
This API call and the following will hammer the control plane every second (drainRequeueImmediate) with at least two calls.
| return ctrl.Result{RequeueAfter: drainRequeueValidate}, nil | ||
| } | ||
|
|
||
| filterRegex := simplyblockv1alpha1.DefaultSystemVolumeFilterRegex |
There was a problem hiding this comment.
This snippet is used at least 4 times. It should be extracted into a helper such as resolveSystemFlter(spec).
…ache custom patterns
Summary
Implements Issue #131 — safe storage node removal with a non-blocking, restartable drain workflow.
Previously
action: removeexpected the node to already be offline. This PR extends the remove action with a multi-step drain phase driven by the existingStorageNodeSetReconciler, so all logical volumes are migrated before the node is shut down.Changes
New: drain-remove state machine (
simplyblockstoragenodeset_drain.go)The reconciler advances through six sub-phases on each reconcile invocation — no blocking loops:
Warningevent if pinned or unmanaged volumes are found. Node is untouched until validation passes.suspend, persistsTriggered=trueto guard against duplicate calls across restarts, polls untilstatus == suspended.VolumeMigrationCR per PV-managed volume. Watches owned CRs via.Owns(&VolumeMigration{}). UpdatesvolumesMigrated/volumesPendingcounters inActionStatus.spec.actionmid-drain, the node is automatically resumed beforeActionStatusis cleared.On any failure after
Suspending,resumeAndFailperforms a best-effort resume so the node returns toonline.New spec fields
New status fields
Other changes
NodeActionShutdown/Restart/Suspend/Resume/Remove constants added to utils/constants.go; all string literals in the controller replaced (fixes goconst lint violation)
RBAC annotations added for volumemigrations, persistentvolumes, persistentvolumeclaims
case "remove" in performNodeAction now returns an explicit error — the action is intercepted in reconcileAction before it can reach that path
Test plan
action: removeon a node with PV-managed volumes — verifyVolumeMigrationCRs are created, node suspends, volumes migrate, node is removedPinnedVolumeBlockingevent and node stays online until annotation is manually removedUnmanagedVolumeBlockingeventspec.actionmid-drain (duringMigrating) — verify node is resumed and returns toonlineMigratingsub-phase — verify drain resumes from the correct sub-phase without duplicate migrationsVolumeMigrationCR fails — verify node is resumed andactionStatus.state = failedrandrwworkload running on PVCs during drain — verify no I/O interruption from node suspend through final removal confirmationOutput
We get event for both blocking condition at the same time