Add watch.labelSelector for label-scoped operator instances - #2049
Open
27rohan wants to merge 1 commit into
Open
Add watch.labelSelector for label-scoped operator instances#204927rohan wants to merge 1 commit into
27rohan wants to merge 1 commit into
Conversation
27rohan
force-pushed
the
feat/watch-label-selector
branch
from
July 28, 2026 17:37
7372224 to
559ccdc
Compare
27rohan
force-pushed
the
feat/watch-label-selector
branch
4 times, most recently
from
July 28, 2026 18:23
d9a5658 to
2fecf66
Compare
27rohan
marked this pull request as ready for review
July 28, 2026 18:36
Allow several operator instances to run side by side, each managing a disjoint label-defined subset of CHI/CHK resources across the same namespaces: - new watch.labelSelector config field (standard Kubernetes label selector syntax) with WATCH_LABEL_SELECTOR env override; invalid selectors abort startup - new watch.requireLabelSelector guard (WATCH_LABEL_SELECTOR_REQUIRED env override): abort startup on an empty selector so a lost or typo'd selector fails loudly instead of silently watching everything - filter CHI/CHK informer events and reconciles by the selector; guard again inside reconcile since requests from owned objects bypass predicates, and confirm ownership on live CR state before any write - treat a label flip-away as unwatch (stop metrics, drop in-memory state) without running the deletion protocol, leaving the CR and its child objects intact for the operator that now matches - auto-exclude selector label keys from label propagation so shard re-assignment never restarts ClickHouse pods - filter metrics-exporter discovery by the same selector so metrics ownership follows reconcile ownership - count skipped CRs in clickhouse_operator_cr_skipped_by_label_selector - document the feature, extend the chopconf CRD schema, config templates, install manifests and helm chart Signed-off-by: Rohan Thakkar <rohant@twitter.com>
27rohan
force-pushed
the
feat/watch-label-selector
branch
from
July 30, 2026 19:27
2fecf66 to
d397981
Compare
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
Today a single operator instance manages every
ClickHouseInstallation(CHI) andClickHouseKeeperInstallation(CHK) in its watched namespaces.watch.namespacesis the only way to split a fleet across operator instances, which does not help when many CHIs live in the same namespaces. At fleet scale this makes one operator both a throughput bottleneck (one slow reconcile delays every other CR) and a blast-radius problem (an operator bug or a bad operator upgrade hits the whole fleet at once, and a canary rollout of a new operator version is impossible).Proposal
A new
watch.labelSelectorsetting lets several operator instances run side by side, each managing a disjoint, label-defined subset of CRs:A typical layout: one operator per shard value, plus a legacy catch-all with
labelSelector: "!example.com/clickhouse-shard"for unlabeled CRs. With one value per shard and an absence-based catch-all, every possible label state matches exactly one operator. This enables gradual operator upgrades (canary one shard), fault isolation, and horizontal scaling of reconcile throughput.Behavior
k8s.io/apimachinery'slabels.Parseaccepts: equality, set-based, presence/absence, conjunctions. An invalid selector aborts startup.WATCH_LABEL_SELECTORandWATCH_LABEL_SELECTOR_REQUIRED. An emptyWATCH_LABEL_SELECTORenv var does not clear a file-configured selector (same semantics asWATCH_NAMESPACES), so an accidentally-unset env var cannot silently widen an operator's scope.statusUpdateProcess(CHI and CHK) validates ownership on the same object snapshot whose resourceVersion fences the update — a flip landing after the read causes a conflict, and the retry re-runs the check. This centrally covers every CR.statuspath (reconcile start, abort, host progress, completion) with no extra API reads for the guard itself; unsharded operators are unaffected. Verified by a race-path test that injects the flip between the snapshot read and the write and asserts the conflicted retry skips.label.exclude, so shard labels never propagate to child objects and flipping a CR's label hands it over without touching running ClickHouse pods.clickhouse_operator_cr_skipped_by_label_selectormetric and logged.Known limitations
Two windows remain open by design, both pre-existing classes of handoff race that also apply to
watch.namespaceschanges today, not regressions introduced here:chi-storage-*auxiliary status ConfigMap is written behind the snapshot ownership check but is a separate object not fenced by the CHI resourceVersion, so a flip landing after the snapshot read can produce one stale ConfigMap write (the CR.statusupdate itself still conflicts and is skipped). It is rewritten by the new owner's next status update; reordering it after the fenced CR write would invert the intentional aux-resources-first ordering, so it is left as-is.In both cases the effect is transient and self-healing: the operator that owns the CR after the flip overwrites the stale data on its next reconcile.
Backward compatibility
Fully opt-in.
labelSelectordefaults to empty andrequireLabelSelectortofalse, preserving today's watch-everything behavior. No API/CRD changes to CHI/CHK themselves; the chopconf CRD schema gains the two optional fields.ClickHouseInstallationTemplates are intentionally not filtered — templates stay visible to every operator instance.Changes
watch.labelSelector+watch.requireLabelSelectorconfig fields, env overrides, startup validationlabel.excludederivation from selector keysdocs/operator_configuration.md), chopconf CRD schema, config templates, install manifests, helm chart values + schemaTesting
go build ./...go test -race -count=1 -vet=off ./pkg/apis/clickhouse.altinity.com/v1/ ./pkg/controller/chi/ ./pkg/controller/chk/ ./pkg/metrics/... ./pkg/chop/... ./cmd/operator/app/— all pass (-vet=offbecausego testvet flags pre-existing non-constant-format-string calls in untouched files on master)Note: manifests were edited by hand in lockstep with
deploy/builder/templates-configbecause the builder scripts are not runnable on macOS (GNU tooling assumptions); happy to regenerate if CI or maintainers run the builders.