Gate SPC deletion + delete-operation on removal of all remaining ApplyDesires - #6661
Gate SPC deletion + delete-operation on removal of all remaining ApplyDesires#6661Chai-bot (redhat-chai-bot) wants to merge 5 commits into
Conversation
…s removal Add a delete-time gate that blocks tearing a cluster down while the ClusterResourcesController still owns ApplyDesires, implemented standalone on main (the clusterresources controller from PR Azure#6070 is not yet present). - internal/api/kubeapplierapi: define ClusterResourcesControllerName, the TagControllerName value the ClusterResourcesController stamps on its ApplyDesires. It mirrors the constant PR Azure#6070 introduces in its clusterresources package and is placed here so the delete gates can reference the value on main; Azure#6070 should consume this constant rather than redefine it. - clusterChildResourcesCleanupController: block ServiceProviderCluster deletion (the document carrying ManagementClusterResourceID) until no ApplyDesires tagged with kubeapplierapi.ClusterResourcesControllerName remain. - operationClusterDelete: hold the delete operation non-terminal until those tagged ApplyDesires are gone (placed after the deadline check so the timeout-failure path still fires). Both gates use the existing kubeApplierDBClients DB-client mechanism with no struct/constructor changes, treat a nil client / nil ManagementClusterResourceID / missing ServiceProviderCluster as "gone", and add table-driven unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds deletion-time gating to ensure a cluster delete doesn’t complete (SPC removal + delete operation terminalization) until kube-applier ApplyDesire documents authored by ClusterResourcesController have been removed.
Changes:
- Introduces a shared tag value (
ClusterResourcesControllerName) underkubeapplierapifor identifying controller-owned desires. - Adds a new precondition to block
ServiceProviderClusterdeletion while controller-taggedApplyDesiresremain. - Adds a new precondition to hold the cluster delete operation non-terminal while controller-tagged
ApplyDesiresremain, plus unit tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/api/kubeapplierapi/tags.go | Adds a new controller tag constant used for deletion gating. |
| backend/pkg/controllers/cluster/deletion/cluster_child_resources_cleanup_controller.go | Blocks SPC deletion until controller-tagged ApplyDesires are gone. |
| backend/pkg/controllers/cluster/deletion/cluster_child_resources_cleanup_controller_test.go | Adds unit tests for the SPC deletion gate + helper. |
| backend/pkg/controllers/cluster/operations/operation_cluster_delete.go | Holds delete operation non-terminal until controller-tagged ApplyDesires are gone. |
| backend/pkg/controllers/cluster/operations/operation_cluster_delete_test.go | Adds unit tests for the operation-level gate behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // cluster-deletion gates can reference the value on main before that package | ||
| // exists. When #6070 lands it should consume this constant rather than | ||
| // redefine its own, so the tag value stays single-sourced. | ||
| ClusterResourcesControllerName = "ClusterResourcesController" |
There was a problem hiding this comment.
Good catch on the convention. I'm intentionally keeping the value as "ClusterResourcesController" here because it must byte-for-byte match the tag that #6070's ClusterResourcesController stamps on its ApplyDesire documents — if this value diverges, the delete gates would match nothing. It's defined in kubeapplierapi specifically so #6070 can consume it rather than redefine its own copy, keeping the value single-sourced.
If we'd prefer to drop the Controller suffix for consistency with the other TagControllerName values, that rename should be done in lockstep with #6070 so the writer and the gates change together. Happy to coordinate that on #6070 if that's preferred.
AI-generated. Review for accuracy.
| remaining := 0 | ||
| for _, desire := range applyDesireIterator.Items(ctx) { | ||
| if desire.Tags == nil { | ||
| continue | ||
| } | ||
| if desire.Tags[kubeapplierapi.TagControllerName] == kubeapplierapi.ClusterResourcesControllerName { | ||
| remaining++ | ||
| } | ||
| } | ||
| if err := applyDesireIterator.GetError(); err != nil { | ||
| return false, utils.TrackError(fmt.Errorf("error iterating ApplyDesires for precondition check: %w", err)) | ||
| } | ||
|
|
||
| return remaining == 0, nil |
There was a problem hiding this comment.
Addressed in dd56f9b — the helper now returns on the first matching tagged desire instead of counting all of them, so it no longer scans every page. iterator.GetError() is checked both on the early exit and after full exhaustion; "gone" is only returned once the iterator is exhausted with no match.
AI-generated. Review for accuracy.
| remaining := 0 | ||
| for _, desire := range applyDesireIterator.Items(ctx) { | ||
| if desire.Tags == nil { | ||
| continue | ||
| } | ||
| if desire.Tags[kubeapplierapi.TagControllerName] == kubeapplierapi.ClusterResourcesControllerName { | ||
| remaining++ | ||
| } | ||
| } | ||
| if err := applyDesireIterator.GetError(); err != nil { | ||
| return false, utils.TrackError(fmt.Errorf("error iterating ApplyDesires for precondition check: %w", err)) | ||
| } | ||
|
|
||
| return remaining == 0, nil |
There was a problem hiding this comment.
Addressed in dd56f9b — same short-circuit here: the gate now returns "not gone" on the first ClusterResourcesController-tagged desire rather than paging through the whole result set.
AI-generated. Review for accuracy.
| { | ||
| name: "tagged ClusterResourcesController ApplyDesire present -> operation held non-terminal", | ||
| spc: newSPC(managementClusterResourceID), | ||
| kubeApplierDesires: []any{taggedDesire("cluster-resource-desire")}, | ||
| // No CS mock: the gate returns before reconcile, so ClusterService must not be called. | ||
| wantStatus: coreapi.ProvisioningStateAccepted, | ||
| }, | ||
| { | ||
| name: "no tagged ApplyDesires -> operation proceeds to reconcile", | ||
| spc: newSPC(managementClusterResourceID), | ||
| kubeApplierDesires: nil, | ||
| setupCSMock: func(ctrl *gomock.Controller, fixture *operationtesting.ClusterTestFixture) ocm.ClusterServiceClientSpec { | ||
| mockCSClient := ocm.NewMockClusterServiceClientSpec(ctrl) | ||
| clusterStatus, _ := arohcpv1alpha1.NewClusterStatus(). | ||
| State(arohcpv1alpha1.ClusterStateUninstalling). | ||
| Build() | ||
| mockCSClient.EXPECT(). | ||
| GetClusterStatus(gomock.Any(), fixture.ClusterInternalID). | ||
| Return(clusterStatus, nil) | ||
| return mockCSClient | ||
| }, | ||
| wantStatus: coreapi.ProvisioningStateDeleting, | ||
| }, | ||
| } |
There was a problem hiding this comment.
Addressed in dd56f9b — added table cases covering the early-return branches: missing/NotFound ServiceProviderCluster, SPC present with a nil ManagementClusterResourceID, and an unregistered management cluster (nil kube-applier client). Each asserts the operation is treated as "gone" and proceeds.
AI-generated. Review for accuracy.
…branches Copilot review feedback on the ClusterResourcesController ApplyDesires delete gate: - clusterResourceApplyDesiresGone (both the child-resources cleanup controller and the delete operation): stop counting all tagged ApplyDesires and instead return "not gone" on the first match, so a single remaining desire short-circuits the listing. The iterator error is still surfaced both on the early exit and after full exhaustion, and "gone" is returned only once the iterator is exhausted with no match. The nil-client / nil ManagementClusterResourceID / missing ServiceProviderCluster short-circuits and log messages are unchanged. - operation_cluster_delete_test: add table cases exercising the early-return "gone -> proceeds" branches (missing ServiceProviderCluster, SPC with a nil ManagementClusterResourceID, and an unregistered management cluster whose kube-applier client is nil). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| // Hold the delete operation non-terminal until the ClusterResourcesController has | ||
| // removed all the ApplyDesires it owns. Placed after the deadline check above so | ||
| // the timeout-failure path still fires if this cleanup stalls. | ||
| applyDesiresGone, err := c.clusterResourceApplyDesiresGone(ctx, cluster) | ||
| if err != nil { | ||
| return utils.TrackError(fmt.Errorf("failed to check ClusterResourcesController ApplyDesire precondition: %w", err)) | ||
| } | ||
| if !applyDesiresGone { | ||
| logger.Info("waiting for ClusterResourcesController to delete its ApplyDesires before completing delete operation") | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The OperationClusterDelete section of docs/cosmos-data-flow.md documents these reads as of the current commit — the ServiceProviderCluster read for Status.ManagementClusterResourceID, then listing ApplyDesires grouped by Tags[ControllerName], with the operation held non-terminal while any remain. No further doc change is needed.
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
| // Check that the ClusterResourcesController has removed all the ApplyDesires it | ||
| // owns. That controller is responsible for deleting its own desires during | ||
| // cluster deletion; we only remove the ServiceProviderCluster document (which | ||
| // carries the ManagementClusterResourceID needed to reach them) once they are gone. | ||
| applyDesiresGone, err := c.clusterResourceApplyDesiresGone( | ||
| ctx, | ||
| spc, | ||
| serviceProviderClusterResourceID.SubscriptionID, | ||
| serviceProviderClusterResourceID.ResourceGroupName, | ||
| clusterName, | ||
| ) | ||
| if err != nil { | ||
| return false, utils.TrackError(fmt.Errorf("failed to check ClusterResourcesController ApplyDesire precondition: %w", err)) | ||
| } | ||
| if !applyDesiresGone { | ||
| logger.Info("waiting for ClusterResourcesController to delete its ApplyDesires before removing ServiceProviderCluster", | ||
| "serviceProviderClusterResourceID", spc.ResourceID.String()) | ||
| return false, nil | ||
| } |
There was a problem hiding this comment.
The ClusterChildResourcesCleanupController section of docs/cosmos-data-flow.md reflects this extra precondition and the added reads — SPC read for Status.ManagementClusterResourceID, ApplyDesire list grouped by Tags[ControllerName], with SPC deletion blocked while any remain. Accurate for the current commit.
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
| continue | ||
| } | ||
| if desire.Tags[kubeapplierapi.TagControllerName] == kubeapplierapi.ClusterResourcesControllerName { | ||
| // A tagged ApplyDesire still exists; the gate is not satisfied. Surface |
There was a problem hiding this comment.
no, count how many we have for each controller and log a message saying we're waiting on X many for each controller to be cleaned up.
There was a problem hiding this comment.
Done in 14ef6f4a. The SPC-deletion gate now counts remaining ApplyDesires grouped by controller (Tags[TagControllerName], untagged bucketed as unknown) and logs a breakdown like "N ApplyDesire(s) still exist: 2 for controller X, 1 for controller unknown" while it blocks.
AI-generated. Review for accuracy.
| // Hold the delete operation non-terminal until the ClusterResourcesController has | ||
| // removed all the ApplyDesires it owns. Placed after the deadline check above so | ||
| // the timeout-failure path still fires if this cleanup stalls. | ||
| applyDesiresGone, err := c.clusterResourceApplyDesiresGone(ctx, cluster) |
There was a problem hiding this comment.
no, do it like hostedClusterDeletionStatus so we can have a message for the operation about how many applydesires are remaining for each controller
There was a problem hiding this comment.
Done in 14ef6f4a. Added applyDesiresDeletionStatus modeled on hostedClusterDeletionStatus (returns *operationbase.OperationState) and wired it into buildDeletionTimeoutMessage (source applyDesires), so the delete operation now carries a per-controller message about how many ApplyDesires remain, and holds non-terminal while any exist.
AI-generated. Review for accuracy.
| // cluster-deletion gates can reference the value on main before that package | ||
| // exists. When #6070 lands it should consume this constant rather than | ||
| // redefine its own, so the tag value stays single-sourced. | ||
| ClusterResourcesControllerName = "ClusterResourcesController" |
There was a problem hiding this comment.
No. The controller name doesn't matter in this PR. Any ApplyDesire should stop deletion
There was a problem hiding this comment.
Done in 14ef6f4a. Reverted the tags.go change entirely — the gate no longer filters by controller name. Any remaining ApplyDesire now blocks deletion.
AI-generated. Review for accuracy.
… counts deads2k review of the ApplyDesire delete gate: - Revert the ClusterResourcesControllerName constant added to internal/api/kubeapplierapi/tags.go. The gate no longer filters by authoring controller, so the value is not needed on main (PR Azure#6070 continues to define it in its own clusterresources package). - Both gates now block on ANY remaining ApplyDesire for the cluster, not just those authored by a particular controller. Each enumerates ApplyDesires via kubeApplierDBClients.For(ctx, mcID).ApplyDesiresForCluster(...).List(...) and builds a count grouped by Tags[kubeapplierapi.TagControllerName] (ApplyDesires with no such tag bucket under "unknown"), emitting a human-readable per-controller breakdown while any remain. - clusterChildResourcesCleanupController: extraDeleteGateShouldDeleteServiceProviderCluster logs the per-controller breakdown and blocks ServiceProviderCluster deletion while any ApplyDesire remains. - operationClusterDelete: applyDesiresDeletionStatus mirrors hostedClusterDeletionStatus, so the delete operation carries a per-controller ApplyDesire status (surfaced via the delete-timeout message) and is held non-terminal while any remain, after the deadline check. - Keep the "gone" short-circuits: nil kube-applier client, nil ManagementClusterResourceID, or missing ServiceProviderCluster are all treated as no remaining ApplyDesires. - Update both packages' tests for the new any-ApplyDesire behavior and the per-controller breakdown, and refresh the OperationClusterDelete and ClusterChildResourcesCleanupController sections of docs/cosmos-data-flow.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| // Hold the delete operation non-terminal until every ApplyDesire for the cluster | ||
| // has been removed, surfacing a per-controller breakdown on the operation status. | ||
| // Placed after the deadline check above so the timeout-failure path still fires if | ||
| // this cleanup stalls. | ||
| applyDesiresState, err := c.applyDesiresDeletionStatus(ctx, cluster) | ||
| if err != nil { | ||
| return utils.TrackError(fmt.Errorf("failed to check remaining ApplyDesires: %w", err)) | ||
| } | ||
| if applyDesiresState.ProvisioningState != coreapi.ProvisioningStateSucceeded { | ||
| logger.Info("waiting for ApplyDesires to be deleted before completing delete operation", "remaining", applyDesiresState.Message) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Updated the PR title and description to match: the gate intentionally blocks while any ApplyDesire remains for the cluster (not only ClusterResourcesController-authored), per review feedback. The per-controller breakdown is for diagnostics only.
AI-generated. Review for accuracy.
There was a problem hiding this comment.
Aligned in 75a66f4. The gate intentionally blocks on any remaining ApplyDesire — there is no controller-specific filtering and no ClusterResourcesControllerName constant. I've updated the PR title to match the implemented behavior. The per-Tags[ControllerName] breakdown is diagnostic only (surfaced in the delete-timeout message), not a gating filter.
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
| parts := make([]string, 0, len(countsByController)) | ||
| for controllerName, count := range countsByController { | ||
| parts = append(parts, fmt.Sprintf("%d for controller %s", count, controllerName)) | ||
| } | ||
| slices.Sort(parts) | ||
| return total, strings.Join(parts, ", "), nil |
There was a problem hiding this comment.
Done in 75a66f4 — the breakdown is now sorted by controller name (in the shared helper) before formatting, so output ordering is stable across reconciles.
AI-generated. Review for accuracy.
There was a problem hiding this comment.
Fixed in 75a66f4 — the breakdown now sorts by controller name (map keys) instead of the formatted "%d for controller %s" strings. This lives in the shared helper kubeappliercosmosstorage.SummarizeApplyDesiresByController and is covered by TestSummarizeApplyDesiresByController.
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
| parts := make([]string, 0, len(countsByController)) | ||
| for controllerName, count := range countsByController { | ||
| parts = append(parts, fmt.Sprintf("%d for controller %s", count, controllerName)) | ||
| } | ||
| slices.Sort(parts) | ||
| return total, strings.Join(parts, ", "), nil |
There was a problem hiding this comment.
Done in 75a66f4. The per-controller breakdown is now sorted by controller name (the map key) before formatting — done once in the shared helper — so ordering is stable across reconciles regardless of counts.
AI-generated. Review for accuracy.
There was a problem hiding this comment.
Done in 75a66f4 — same shared helper (SummarizeApplyDesiresByController) now sorts by controller name, so this path is stable too.
AI-generated. Review for accuracy.
There was a problem hiding this comment.
Fixed in 75a66f4 — same sort-by-controller-name fix, now centralized in the shared helper SummarizeApplyDesiresByController used by both this controller and the delete operation (the byte-for-byte duplicate here was removed).
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
There was a problem hiding this comment.
Done in 75a66f4. The breakdown now sorts by controller name (sorting the map keys, then formatting) instead of the "%d for controller %s" string, so ordering is stable across reconciles regardless of counts. The logic is centralized in the shared helper kubeappliercosmosstorage.SummarizeApplyDesiresByController, and TestSummarizeApplyDesiresByController ("breakdown sorted by controller name, not count") asserts the ordering.
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
David Eads (deads2k)
left a comment
There was a problem hiding this comment.
fixes needed.
| parts := make([]string, 0, len(countsByController)) | ||
| for controllerName, count := range countsByController { | ||
| parts = append(parts, fmt.Sprintf("%d for controller %s", count, controllerName)) | ||
| } | ||
| slices.Sort(parts) | ||
| return total, strings.Join(parts, ", "), nil |
| // has been removed, surfacing a per-controller breakdown on the operation status. | ||
| // Placed after the deadline check above so the timeout-failure path still fires if | ||
| // this cleanup stalls. | ||
| applyDesiresState, err := c.applyDesiresDeletionStatus(ctx, cluster) |
There was a problem hiding this comment.
no! put the computation in buildDeletionTimeoutMessage with the rest of our calculations. And re-use the logic for associating applydesires with controllers.
There was a problem hiding this comment.
Done in 75a66f4. Removed the standalone applyDesiresDeletionStatus; the remaining-ApplyDesires computation is folded into buildDeletionTimeoutMessage alongside the other deletion-timeout calculations. The "enumerate ApplyDesires + associate with controllers" logic is now a single shared helper — SummarizeApplyDesiresByController in kubeappliercosmosstorage — reused by both buildDeletionTimeoutMessage/the operation gate and the child-cleanup gate (no more duplicated copies).
AI-generated. Review for accuracy.
There was a problem hiding this comment.
Done in 75a66f4. The per-controller computation now lives with the other deletion-status calculations in buildDeletionTimeoutMessage (via the shared countRemainingApplyDesires data helper, emitted as a .WithSource("applyDesires") state). The SynchronizeOperation gate reuses that same countRemainingApplyDesires rather than computing its own, and the controller-association logic is now the single shared helper kubeappliercosmosstorage.SummarizeApplyDesiresByController. The standalone applyDesiresDeletionStatus is gone.
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
…calc into buildDeletionTimeoutMessage deads2k review follow-ups: - Sort the per-controller ApplyDesire breakdown by controller NAME before formatting (previously the formatted "%d for controller %s" strings were sorted, so ordering depended on counts and drifted across reconciles). - Factor the "enumerate ApplyDesires + group by Tags[kubeapplierapi.TagControllerName] (untagged -> unknown), sorted by name" logic into a single shared helper, kubeappliercosmosstorage.SummarizeApplyDesiresByController, and reuse it from BOTH the ClusterChildResourcesCleanupController SPC-deletion gate and the delete operation. Removes the two duplicated per-package copies. Placed in kubeappliercosmosstorage, which both controllers already import and which owns the ApplyDesire CRUD (no import cycle). - operationClusterDelete: remove the standalone applyDesiresDeletionStatus and fold the "remaining ApplyDesires per controller" computation into buildDeletionTimeoutMessage alongside the other deletion-timeout calculations. A small countRemainingApplyDesires data helper (SPC -> kube-applier client -> shared summarizer) is reused by both buildDeletionTimeoutMessage and the SynchronizeOperation gate, which still holds the operation non-terminal while any ApplyDesire remains (after the deadline check). Behavior unchanged: gate on ANY remaining ApplyDesire (no controller-name filter); nil client / nil ManagementClusterResourceID / missing ServiceProviderCluster are treated as gone/proceed. tags.go remains untouched vs origin/main. Tests updated for the stable name-sorted breakdown, and a direct unit test covers the shared summarizer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: redhat-chai-bot The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
backend/pkg/controllers/cluster/operations/operation_cluster_delete.go:193
- The PR description states these new delete gates are scoped to ApplyDesires authored by ClusterResourcesController (via Tags[TagControllerName] == ClusterResourcesControllerName), but the implementation here gates on any remaining ApplyDesire for the cluster and tags are only used for grouping in the breakdown. Please align either the implementation (filter by the specific controller tag) or the PR description/docs so reviewers and operators have the same understanding of what blocks deletion.
// Hold the delete operation non-terminal until every ApplyDesire for the cluster
// has been removed. Placed after the deadline check above so the timeout-failure
// path still fires if this cleanup stalls; that path (buildDeletionTimeoutMessage)
// surfaces the same per-controller breakdown on the operation.
remainingApplyDesires, applyDesireBreakdown, err := c.countRemainingApplyDesires(ctx, cluster)
if err != nil {
return utils.TrackError(fmt.Errorf("failed to check remaining ApplyDesires: %w", err))
}
if remainingApplyDesires > 0 {
logger.Info("waiting for ApplyDesires to be deleted before completing delete operation", "remaining", applyDesireBreakdown)
return nil
}
backend/pkg/controllers/cluster/operations/operation_cluster_delete.go:199
- Returning early when ApplyDesires remain prevents reconcileOperationAndResourceStatus from running, which can leave the delete Operation status stale (e.g., staying in Accepted) for long periods even while Cluster Service reports Uninstalling. If the goal is only to prevent the operation from becoming terminal, consider allowing status reconciliation to proceed and only gating terminal completion (or explicitly document that this controller intentionally stops polling Cluster Service until ApplyDesires are gone).
// Hold the delete operation non-terminal until every ApplyDesire for the cluster
// has been removed. Placed after the deadline check above so the timeout-failure
// path still fires if this cleanup stalls; that path (buildDeletionTimeoutMessage)
// surfaces the same per-controller breakdown on the operation.
remainingApplyDesires, applyDesireBreakdown, err := c.countRemainingApplyDesires(ctx, cluster)
if err != nil {
return utils.TrackError(fmt.Errorf("failed to check remaining ApplyDesires: %w", err))
}
if remainingApplyDesires > 0 {
logger.Info("waiting for ApplyDesires to be deleted before completing delete operation", "remaining", applyDesireBreakdown)
return nil
}
if !c.shouldReconcileOperationAndResourceStatus(cluster) {
return nil
}
err = c.reconcileOperationAndResourceStatus(ctx, operation, cluster)
if err != nil {
| func SummarizeApplyDesiresByController(ctx context.Context, applyDesireCRUD cosmosstorageutils.ResourceCRUD[kubeapplierapi.ApplyDesire, *kubeapplierapi.ApplyDesire]) (int, string, error) { | ||
| applyDesireIterator, err := applyDesireCRUD.List(ctx, &cosmosstorageutils.DBClientListResourceDocsOptions{}) | ||
| if err != nil { | ||
| return 0, "", fmt.Errorf("failed to list ApplyDesire documents: %w", err) | ||
| } | ||
|
|
||
| countsByController := map[string]int{} | ||
| total := 0 | ||
| for _, desire := range applyDesireIterator.Items(ctx) { | ||
| controllerName := desire.Tags[kubeapplierapi.TagControllerName] | ||
| if controllerName == "" { | ||
| controllerName = UnknownApplyDesireController | ||
| } | ||
| countsByController[controllerName]++ | ||
| total++ | ||
| } |
There was a problem hiding this comment.
The full enumeration is intentional. deads2k asked for a per-controller count breakdown in the wait message ("count how many we have for each controller and log a message saying we're waiting on X many for each controller"), which requires visiting every ApplyDesire — a boolean short-circuit can't produce per-controller counts. The gate blocks while total > 0, so this only pages the full set during the (bounded) tail of deletion while desires are still being cleaned up. If the scan cost becomes a concern for very large clusters, a follow-up could cap/paginate the breakdown, but that's out of scope for this PR.
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
| if remainingApplyDesires > 0 { | ||
| logger.Info("waiting for ApplyDesires to be deleted before completing delete operation", "remaining", applyDesireBreakdown) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Done in c54c328. The delete-operation wait log now logs the numeric count under remaining (an int) and moves the human-readable breakdown to a separate breakdown field — so remaining is queryable/alertable as an integer:
logger.Info("waiting for ApplyDesires to be deleted before completing delete operation", "remaining", remainingApplyDesires, "breakdown", applyDesireBreakdown)The cleanup controller's SPC-deletion gate log builds the count + breakdown into the message via fmt.Sprintf (its only structured field is serviceProviderClusterResourceID), so it doesn't have the "remaining"=string pattern and was left as-is.
AI-generated. Review for accuracy.
AI-generated. Review for accuracy.
|
/retest
AI-generated. Review for accuracy. |
…er separate key The SynchronizeOperation delete gate's structured log set the "remaining" field to the human-readable breakdown string instead of the numeric count. Log the numeric remainingApplyDesires count under "remaining" and move the breakdown string to a separate "breakdown" field (Copilot, operation_cluster_delete.go). The ClusterChildResourcesCleanupController SPC-deletion gate log already embeds the count and breakdown in the message via fmt.Sprintf (no structured "remaining" field), so it needs no change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Chai Bot <ship-help-github@redhat.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
backend/pkg/controllers/cluster/deletion/cluster_child_resources_cleanup_controller.go:357
- remainingApplyDesires() already returns an error to a caller that wraps it with utils.TrackError. Wrapping this error with utils.TrackError here can lead to double-tracked / noisy errors. Prefer returning a plain wrapped error and let the caller track once.
This issue also appears on line 359 of the same file.
applyDesireCRUD, err := kubeApplierDBClient.ApplyDesiresForCluster(subscriptionID, resourceGroupName, clusterName)
if err != nil {
return 0, "", utils.TrackError(fmt.Errorf("failed to get kube-applier CRUD for ApplyDesire precondition: %w", err))
}
backend/pkg/controllers/cluster/operations/operation_cluster_delete_test.go:417
- This test uses time.Now() even though the controller is configured with a fake clock (fixedTime). Using wall-clock time makes the test less deterministic and can become flaky/confusing if any logic compares timestamps to c.clock.Now(). Prefer using fixedTime so the entire test is time-stable.
clusterPassingReconcileGate := func() *coreapi.HCPOpenShiftCluster {
now := time.Now()
cluster := fixture.NewCluster(nil)
cluster.ServiceProviderProperties.DeletionTimestamp = &metav1.Time{Time: now}
cluster.ServiceProviderProperties.ClusterServiceDeletionTimestamp = &metav1.Time{Time: now}
return cluster
backend/pkg/controllers/cluster/deletion/cluster_child_resources_cleanup_controller.go:362
- Same double-tracking concern as above: returning utils.TrackError(err) here means the caller will TrackError again. Return a plain wrapped error instead so tracking happens once at the top-level.
total, breakdown, err := kubeappliercosmosstorage.SummarizeApplyDesiresByController(ctx, applyDesireCRUD)
if err != nil {
return 0, "", utils.TrackError(err)
}
|
Chai-bot (@redhat-chai-bot): The following test failed, say
Full PR test history. Your PR dashboard. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here. |
Summary
Blocks removal of a cluster's
ServiceProviderCluster(SPC) document and holds the cluster delete operation non-terminal while any kube-applierApplyDesirestill exists for the cluster. This ensures controllers finish deleting theirApplyDesires before the SPC/cluster is torn down.Per review, the gate is intentionally controller-agnostic — any remaining
ApplyDesireblocks deletion, regardless of which controller authored it. The per-controller breakdown is surfaced purely for diagnostics.Changes
backend/pkg/controllers/cluster/deletion/cluster_child_resources_cleanup_controller.go—extraDeleteGateShouldDeleteServiceProviderClusternow blocks SPC deletion while anyApplyDesireremains, logging a per-controller breakdown.backend/pkg/controllers/cluster/operations/operation_cluster_delete.go— the remaining-ApplyDesirecomputation is folded intobuildDeletionTimeoutMessagealongside the other deletion-timeout calculations;SynchronizeOperationholds the operation non-terminal (after the deadline check) while any remain, and the operation message reports how manyApplyDesires are left per controller.internal/database/cosmosstorage/kubeappliercosmosstorage/apply_desire_summary.go— new sharedSummarizeApplyDesiresByControllerhelper (enumerateApplyDesires, group byTags[TagControllerName]with untagged bucketed asunknown, sorted by controller name). Reused by both gates so the association logic lives in one place.docs/cosmos-data-flow.md— updated theOperationClusterDeleteandClusterChildResourcesCleanupControllersections to document the new reads.Behavior notes
ServiceProviderCluster, a nilManagementClusterResourceID, or no kube-applier client for the management cluster.Validation
make -C backend build→ exit 0go testforcluster/deletion,cluster/operations, andkubeappliercosmosstorage→ all pass