Filter kube informers server-side to operator-generated objects - #2058
Open
gregakinman wants to merge 1 commit into
Open
Filter kube informers server-side to operator-generated objects#2058gregakinman wants to merge 1 commit into
gregakinman wants to merge 1 commit into
Conversation
gregakinman
marked this pull request as draft
August 5, 2026 19:32
gregakinman
force-pushed
the
informer-scope-fix
branch
from
August 5, 2026 19:35
2f1a148 to
8b1e200
Compare
The kube informer factory watches Pods, Services, EndpointSlices, ConfigMaps and StatefulSets across the whole cluster whenever watchNamespaces is anything other than a single literal namespace name, because GetInformerNamespace() falls back to meta.NamespaceAll in every other case, including regexp patterns. Filtering then happens client-side in isTrackedObject(), i.e. after every object in the cluster has already been transferred, decoded and queued in the DeltaFIFO. Informer memory therefore scales with total cluster size rather than with the number of managed installations, which OOM-kills the operator on large clusters with many namespaces. Push the same filter server-side with WithTweakListOptions. The selector is built through the same labeler that IsCHOPGeneratedObject() resolves through, rather than from the raw LabelAppName constant, so the server-side selector and the client-side predicate cannot drift apart. This is intended to be behavior-preserving: every handler registered on the kube informer factory already gates on isTrackedObject(), and nothing reads these caches expecting to find objects the operator did not generate - there are no Lister() or GetIndexer() reads against them anywhere in the tree. CHOp custom resource informers are deliberately left unfiltered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gregakinman
force-pushed
the
informer-scope-fix
branch
from
August 5, 2026 21:40
8b1e200 to
ca17d72
Compare
gregakinman
marked this pull request as ready for review
August 5, 2026 21:52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Problem
The kube informer factory watches Pods, Services, EndpointSlices, ConfigMaps and StatefulSets across the whole cluster whenever
watchNamespacesis anything other than a single literal namespace name —GetInformerNamespace()returnsmeta.NamespaceAllin every other case, including regexp patterns.Filtering then happens client-side in
isTrackedObject(), i.e. after every object in the cluster has already been transferred, decoded and queued in the DeltaFIFO. Informer memory therefore scales with total cluster size rather than with the number of managed installations, which OOM-kills the operator on large clusters with many namespaces.Change
Push the same filter server-side with
kubeinformers.WithTweakListOptions, settingopts.LabelSelectorto the chop-generated-object label.The selector is built through the same labeler that
IsCHOPGeneratedObject()resolves through, rather than from the rawLabelAppNameconstant — that constant is the literal placeholder"APIGroupName" + "/" + "app"and is substituted at runtime, so building the selector any other way would let it drift from the client-side predicate. Resolved, it isclickhouse.altinity.com/app=chop.CHOp custom resource informers (CHI/CHIT/CHOpConfig) are deliberately left unfiltered — they are cluster-wide by necessity and small.
24 added lines in one file, plus tests. Nothing is removed or renamed, and no default changes.
Why this is behavior-preserving
kubeInformerFactoryalready gates onisTrackedObject(), which requiresIsCHOPGeneratedObject(). No handler regresses.Lister()orGetIndexer()reads against the kube informers anywhere in the tree, so they function purely as event sources.Two cases worth a reviewer's attention:
EndpointSlices are populated by Kubernetes, not by the operator. They inherit the label from the chop-created Service, since the endpointslice controller mirrors Service labels onto slices except for reserved prefixes. Verified on two live clusters — 179/179 and 19/19 chop-labelled Services had a correspondingly labelled EndpointSlice, zero misses. The legacy
Endpointsinformer is never constructed (addEventHandlersEndpointsis commented out at the registration site), so it is unaffected either way.CHI and CHK use different label keys.
clickhouse.altinity.com/appversusclickhouse-keeper.altinity.com/app, and a label selector cannot express a disjunction over two keys. This is fine becauseisTrackedObject()uses the CHI labeler exclusively, so CHK-generated objects were already dropped client-side; the test table pins that case so it cannot silently regress.One pre-existing sharp edge this does not change: CR-provided labels are merged over generated ones, so a CHI that sets
clickhouse.altinity.com/appto another value strips the label from its own generated objects. That already defeatedisTrackedObject()identically, so the net behavior is the same — no reconcile either way.Testing
go build ./...clean.go vetproduces an identical finding set before and after, compared in the same tree with the same build cache — no new findings.IsCHOPGeneratedObject()across a table of label sets, including the wrong-value, bare-app, and keeper-labelled cases.go testcurrently needs-vet=offon master due to pre-existing vet failures, andTestEnforceVerifiedLegacyTLSfails on macOS independently of this change.On one production-shaped cluster with a regexp watch config, cached objects across the five watched types drop from 22,194 to 761.
🤖 Generated with Claude Code