Skip to content

KUBE-179: clean up and self-heal MongoDBSearch resources#1382

Draft
anandsyncs wants to merge 2 commits into
search-kube154-label-sweep-lifecyclefrom
search-kube154-cleanup-self-healing
Draft

KUBE-179: clean up and self-heal MongoDBSearch resources#1382
anandsyncs wants to merge 2 commits into
search-kube154-label-sweep-lifecyclefrom
search-kube154-cleanup-self-healing

Conversation

@anandsyncs

@anandsyncs anandsyncs commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Jira: KUBE-179

Stack

This is stack 2/4 for KUBE-154, based on #1362 (search-kube154-label-sweep-lifecycle) at the accepted layer-1 head 5aa0e085809d2d414485d122a7f789a3b6e0a265.

Landing history

  1. 74af473be71058fa747a7e99d23a6674ccf8c9ffKUBE-154: sweep deleted MongoDBSearch resources safely
  2. 90e0eae8c5226ee8cb1259260aaf1d39fa675b3dKUBE-154: route MongoDBSearch resource lifecycle events

Both commits are linear on the accepted layer-1 head, include their production behavior and focused tests together, and carry the Copilot co-author trailer.

Scope

  • Confirms cached MongoDBSearch NotFound results through uncached readers, then performs fail-closed, label-scoped cleanup across central and member clusters for Deployment, StatefulSet, Service, ConfigMap, and Secret only.
  • Live-reads the same MongoDBSearch through each target cluster's API reader before deleting a candidate: matching local-parent UIDs and UID-less resources are preserved, while explicit non-local UIDs remain eligible after the central absence check. A missing Search GVK is treated as authoritative absence; ordinary API errors still fail closed. This prevents central simulated-MC operators from deleting member-local Search resources while allowing Q2 cleanup on clusters without the Search CRD.
  • Uses UID and resource-version delete preconditions, stops writes while deletion is active, removes legacy Search owner references, and cleans stale shard/topology and generated TLS resources without directly sweeping Pods, ReplicaSets, or PVCs.
  • Requires the exact non-empty current Search UID in every stale-shard list selector and the final pre-delete ownership guard. Different-UID and UID-less resources are preserved. Secret sweeping remains active after TLS is disabled so stale operator-generated per-shard TLS copies are removed.
  • Preserves metrics-forwarder host deregistration ordering, durably records topology before resource writes, and safely cleans current, removed, and historical cluster resources. Historical state without a trustworthy cluster index fails closed.
  • Routes create, update, label-removal, and delete lifecycle events for all five managed kinds; ConfigMap/Secret delete routing remains Search-only.
  • Adds central/member Envoy and metrics Deployment/ConfigMap self-healing watches without adding another topology branch or status writer.
  • Honors the canonical disable-reconciliation annotation in Envoy and metrics reconcilers after deletion/finalizer handling but before ordinary writes, preserving CDS hot-reload test mutations and metrics-forwarder pause semantics.

Intentionally excluded

  • No Python/e2e changes, shared Python helpers, .evergreen.yml, or operator_upgrade_search.py.
  • No new finalizer, periodic reaper, cleanup ledger, CRD, or direct descendant sweep.

Validation

  • Cleanup commit independently: go test ./controllers/operator ./controllers/searchcontroller ./pkg/handler ./pkg/tls -count=1.
  • Cumulative relevant Go packages passed with the same command.
  • make fmt, repository formatter, and git diff --check passed.
  • make test-race: 2,926 tests passed, 3 skipped.
  • Fresh read-only MCK/code review found no new actionable issue; historical metrics state without a trustworthy cluster index remains intentionally fail-closed.
  • Exhaustive patch 6a5a282e8ee1c90007dbd20b: 67 of 68 tasks succeeded. The exact scope was 56 requested targets plus 12 dependencies, and all 68 generated tasks used execution setting 30.
  • Focused connectivity patch 6a5a3c34df5cdd0007d4df78: all nine tasks succeeded (e2e_search_connectivity_tool_mc_sharded plus eight dependencies), with every generated task using execution setting 30.
  • Requested targets: 34 e2e_mdb_kind_ubi_cloudqa_large Search tasks; one Search-block task; one OpenShift Search task; four Ops Manager 8.0 metrics-forwarder tasks; two simulated multi-cluster tasks; five two-cluster Search tasks; one operator-upgrade Search task; seven Search snippet tasks; and unit_tests_golang.
  • Dependencies: ten init_test_run image/chart tasks plus build_om_images on the Ops Manager 7.0 and 8.0 build variants.
  • The sole failure was e2e_operator_kind_ubi_cloudqa/e2e_operator_upgrade_search in both executions: test_database_running_after_upgrade timed out after 300 seconds while mdb-rs-2 remained at automation goal -1. This is the accepted pre-Layer-4 upgrade gap and was not retried or attributed to Layer 2.
  • All Search connectivity targets were executed. The pushed-code patch also recorded successful replica-set and sharded multi-cluster connectivity tasks; the focused patch above is the authoritative sharded result.
  • HEAD~2 is exactly 5aa0e085809d2d414485d122a7f789a3b6e0a265; the branch contains exactly the two commits listed above and has tree e3b69354fa29f1466222ec1f138fe2315bd26455.

Broad webhook patches are non-evidence. Automatic patch 6a5a1cbc50e4c1000765a00c was canceled after its relevant connectivity tasks succeeded. Earlier targeted patch 6a58d3d7ad18c500078c3722 and exhaustive patch 6a58dc67aa73bb00071726c2 validated a superseded base/tree and are retained only as historical context.

skip-ci

@anandsyncs
anandsyncs force-pushed the search-kube154-cleanup-self-healing branch 2 times, most recently from 7416fbe to 9e05f7b Compare July 16, 2026 01:37
@anandsyncs
anandsyncs force-pushed the search-kube154-cleanup-self-healing branch 2 times, most recently from 7787462 to 79421f8 Compare July 16, 2026 12:49
@anandsyncs
anandsyncs requested a review from Copilot July 17, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR advances KUBE-154 stack 2/4 by shifting MongoDBSearch-managed resource lifecycle tracking and cleanup from cross-cluster OwnerReferences to label-based identity, then adding “self-heal” and fail-closed cleanup behaviors across central/member clusters.

Changes:

  • Remove OwnerReferences from Search-owned resources and rely on stable, protected “search-owner” labels for event routing and garbage-collection workflows.
  • Add NotFound “confirmed absence” cleanup that double-checks with uncached readers and performs label-scoped, UID-/RV-preconditioned deletions across central and member clusters.
  • Add/adjust controllers and watches (main Search, Envoy, metrics-forwarder) to route lifecycle events via label mappers and to honor deletion/disable-reconciliation short-circuits.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/tls/tls_secret.go Stops setting owner references on operator-managed TLS secrets.
pkg/handler/labels_search.go Updates label semantics/docs for Search routing and cleanup.
controllers/searchcontroller/search_state.go Migrates Search state ConfigMap metadata away from OwnerReferences to labels.
controllers/searchcontroller/search_state_test.go Updates state tests for label-only identity and owner-ref removal.
controllers/searchcontroller/search_construction.go Removes STS owner-reference stamping and updates comments for label lifecycle.
controllers/searchcontroller/mongodbsearch_reconcile_helper.go Introduces cleanup error propagation; converts identity helpers to label-only; expands stale-shard cleanup.
controllers/searchcontroller/mongodbsearch_reconcile_helper_test.go Updates reconcile tests for no owner refs and adds stale-shard cleanup coverage.
controllers/operator/watch/config_change_handler.go Adds optional MapFunc routing to support Search lifecycle mapping on create/update/delete.
controllers/operator/watch/config_change_handler_test.go Adds coverage for delete routing and MapFunc-driven event mapping behavior.
controllers/operator/state_store.go Adds reader injection (cached vs uncached) and simplifies owner-ref handling in state storage.
controllers/operator/mongodbsearchenvoy_controller.go Adds deletion/disable short-circuits; converts watches to label-based mapping; removes owner refs from Envoy resources.
controllers/operator/mongodbsearchenvoy_controller_test.go Updates/extends envoy tests for label-based routing, pause semantics, and watch registration.
controllers/operator/mongodbsearch_metrics_controller.go Adds historical topology cleanup, uncached readers, and removes owner refs; tightens deletion/removed-cluster behavior.
controllers/operator/mongodbsearch_metrics_controller_test.go Adds extensive coverage for topology persistence, deletion semantics, and removed-cluster cleanup.
controllers/operator/mongodbsearch_controller.go Implements confirmed-NotFound cleanup sweep using uncached readers and label/UID/RV preconditions; rewires watches to label mapping.
controllers/operator/mongodbsearch_controller_test.go Adds/updates tests for NotFound sweep lifecycle, uncached reader confirmation, and watch behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread controllers/searchcontroller/mongodbsearch_reconcile_helper.go
@anandsyncs
anandsyncs force-pushed the search-kube154-label-sweep-lifecycle branch from 42c8208 to 5aa0e08 Compare July 17, 2026 12:00
anandsyncs and others added 2 commits July 17, 2026 14:03
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@anandsyncs
anandsyncs force-pushed the search-kube154-cleanup-self-healing branch from 79421f8 to 90e0eae Compare July 17, 2026 12:14
@anandsyncs anandsyncs changed the title KUBE-154: clean up and self-heal MongoDBSearch resources KUBE-179: clean up and self-heal MongoDBSearch resources Jul 17, 2026
op, err := controllerutil.CreateOrUpdate(ctx, kubeClient, sts, func() error {
statefulset.Apply(modifications...)(sts)
return controllerutil.SetOwnerReference(r.mdbSearch, sts, kubeClient.Scheme())
sts.OwnerReferences = nil

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design decision reviewers should ratify — owner-ref removal without a finalizer: this PR removes owner references from every central-cluster managed resource (STS here, Services/ConfigMap, TLS secret, Envoy, state CMs) while the search and Envoy controllers gain no finalizer (only the metrics controller keeps one). Master relied on operator-independent k8s GC for central resources; after this PR, cleanup is entirely operator-dependent — if the operator is down when the CR is deleted (nothing holds the CR), or the sweep fails closed permanently, central resources leak until a later reconcile. Two cheap outs: keep owner refs on central-cluster writes only (harmless alongside the UID-scoped sweep, restores the GC backstop; cross-cluster refs stay removed since they're the actual bug), or add the canonical finalizer like the metrics controller. Worth an explicit decision in review rather than an implicit one.

return reconcile.Result{}, nil
}

// Short-circuit: the disable-reconciliation annotation allows to

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation check is copy-pasted verbatim in all three controllers (here, mongodbsearch_metrics_controller.go, mongodbsearchenvoy_controller.go). Suggest a func (s *MongoDBSearch) IsReconciliationDisabled() bool on the API type so the semantics can't drift three ways.

// member-cluster resource owned by this MongoDBSearch must carry. Owner refs
// don't cross cluster boundaries; labels are the only path back from a member
// resource to its central CR (for watch routing and label-based GC).
func searchOwnerLabels(search *searchv1.MongoDBSearch, clusterName string) map[string]string {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

searchOwnerLabels, envoyLabelsForCluster (mongodbsearchenvoy_controller.go), and metricsForwarderLabelsForCluster (mongodbsearch_metrics_controller.go) build the same owner-name/namespace/uid/cluster map and differ only in app/component values. One searchManagedLabels(search, component, appName, clusterName) would remove the three-way drift risk. (Related nit: the 5-managed-kinds set is enumerated in ~4 places — watch wiring ×3 and the sweep's orderedKinds.)


if mdbSearch.Status.Version == "" {
return workflow.Pending("Waiting for MongoDBSearch version to be reconciled")
func (r *MongoDBSearchMetricsForwarderReconciler) reconcileCore(ctx context.Context, mdbSearch *searchv1.MongoDBSearch, log *zap.SugaredLogger) workflow.Status {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reconcileCore is ~193 lines with interleaved deleting / cleaningRemovedOperator branches and finalizer removal inlined in multiple places. The read-once/write-once topology-state flow is a real improvement — but consider extracting the removed-operator-cluster path and the deletion/finalizer path into named functions so the happy path reads linearly.

khandler.MongoDBSearchOwnerNamespaceLabel: nsName.Namespace,
}

clusters := []struct {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this 4-field anonymous struct type is written out three times (slice literal + appends). Naming it (type sweepCluster struct{ name string; listReader, parentReader client.Reader; deleteClient kubernetesClient.Client }) makes the sweep signature self-documenting.

return errors.Join(errs, xerrors.Errorf("MongoDBSearch %s was recreated during cleanup; aborting %s deletion on cluster %q", search, kind, clusterName))
}
log.Infof("Deleting %s %s for deleted MongoDBSearch %s on cluster %q", kind, obj.GetName(), search, clusterName)
if err := deleteClient.Delete(ctx, obj, client.Preconditions{

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion: the UID precondition is the load-bearing guard here (delete only the exact incarnation). The resourceVersion precondition on top adds little — nothing should be mutating a resource whose owner is confirmed gone — and a benign concurrent update (e.g. kubelet status write) turns a clean delete into a 409 that fails the whole kind's sweep and requeues. Also, on the central cluster parentReader == uncachedReader, so the per-item eligibility re-read duplicates the sweep-top absence confirmation — N extra uncached GETs. Suggest: keep UID, drop RV, skip the central re-read.

for k, v := range memberClustersMap {
clientsMap[k] = kubernetesClient.NewClient(v)
}
readerForMemberCluster := func(clusterName string) client.Reader {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: uncachedReader/readerForMemberCluster are initialized to cached values here and only overridden with GetAPIReader() in AddMongoDBSearchController — so unit tests exercise different (cached) read semantics than production for the one field whose whole purpose is uncached live-reads. Prefer passing the API reader as a constructor argument set once.

gone(search.MongotStatefulSetForClusterShard(0, "x-svc"), &appsv1.StatefulSet{}, "stale x-svc STS reaped despite name-collision with live x headless Service")
}

func TestOwnsForSweepRequiresCurrentSearchUID(t *testing.T) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant micro test — all three branches (current UID → sweep eligible; different UID / missing UID → preserve) are already pinned by TestCleanupStaleShardResources and, at reconciler level, by TestMongoDBSearchReconcile_NotFoundSweepsOwnedResourcesAcrossClusters. Testing a helper directly when the same behavior is observable through the reconcile path doubles the maintenance surface for zero coverage. Suggest deleting.

}
}

func TestCleanupStaleShardResources_UnmanagedLB(t *testing.T) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and TestCleanupStaleShardResources_TLSDisabledDeletesCurrentUIDSecret each differ from TestCleanupStaleShardResources by exactly one input (LB mode / TLS off) — fold them in as table rows (~50 lines saved, one place to read the sweep matrix). Separately, the higher-value addition: the stale-shard sweep (including the new TLS-secret reaping) is only ever invoked directly in tests — no test reduces the shard set through a top-level Reconcile and asserts the sweep is actually wired into the reconcile path. One reconciler-level test would close that.

}
}

func TestResourcesHandlerMapFuncEvents(t *testing.T) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event routing is proven at 2-3 levels: this table duplicates TestMongoDBSearchDeploymentWatchesRouteLifecycleEvents (controller-level, real registered predicates) and the delete rows of TestResourcesHandlerDelete. Suggest dropping this one and keeping TestResourcesHandlerDelete + a single create/update case as the watch-package unit; the controller-level test already proves the wiring end to end.

assert.True(t, apierrors.IsNotFound(err), "expected cleanup of the persisted removed cluster before finalizer removal")
}

func TestReconcile_DeletionFailsClosedForRemovedClusterWithoutPersistedIndex(t *testing.T) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Five near-duplicate reconciler tests → two tables: the three fails-closed variants (DeletionFailsClosedForRemovedClusterWithoutPersistedIndex, RemovedClusterWithoutPersistedIndexFailsClosed, RemovedPerClusterOperatorFailsClosedWithoutPersistedTopology) differ only in {deleting?, operatorClusterName, persisted-topology}; the two cleans-persisted variants (DeletionCleansPersistedRemovedClusterInPerClusterMode, RemovedPerClusterOperatorCleansPersistedResources) share the same shape. Table-driving both families saves ~90 lines and makes the removed-cluster/per-cluster-operator matrix legible at a glance.

}
}

func TestMongoDBSearchReconcile_DeletionTimestampShortCircuits(t *testing.T) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small things: (1) CachedNotFoundPreservesRecreatedSearchResources and NotFoundSweepStopsWhenSearchIsRecreated are a natural 2-row table (pre-sweep vs mid-sweep recreate). (2) A missing interaction test: the disable-reconciliation pause and the deletion path are each tested in isolation across all three controllers, but never together — does an annotation-paused CR still get its cleanup handling on delete, and in which order? One reconciler test pinning that ordering would prevent a silent behavior change.

@anandsyncs

Copy link
Copy Markdown
Contributor Author

Go test audit summary (companion to the inline notes). The reconciler-level core here is comprehensive and well-leveled: the NotFound-sweep matrix, the fail-closed/missing-GVK/UID-precondition cases, and the metrics deletion family are all driven through Reconcile, and the interceptor-based edge tests are legitimately unreachable through the public path. The cleanup opportunities are concentrated: (a) the owner-ref assertion churn inherited from #1362 (~40-50 lines rewritten in this PR), (b) two redundant micro tests (TestOwnsForSweepRequiresCurrentSearchUID, TestResourcesHandlerMapFuncEvents) and two mergeable stale-sweep variants (~130 lines, zero coverage loss), and (c) five table-mergeable metrics deletion tests (~90 lines). One genuinely missing reconciler-level scenario: shard-count reduction driven through Reconcile asserting the stale-shard sweep (incl. TLS-secret reaping) is wired in — the sweep matrix is helper-only today.

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.

2 participants