feat(controllers): Enable multiple controllers with different label selectors for the same entity type (V2)#1070
Conversation
…electors for the same entity type
@dotnet-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Adds support for registering multiple IEntityController<TEntity> implementations for the same entity type and dispatching reconciliation/deletion events to all controllers whose label selector matches the entity’s labels.
Changes:
- Adds
LabelFilteras a defaulted property onIEntityController<TEntity>to allow per-controller label scoping. - Updates DI registration to allow multiple scoped controller registrations for the same entity type.
- Introduces runtime dispatch in
Reconciler<TEntity>and aLabelSelectorMatcherutility with accompanying unit tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/KubeOps.Operator.Test/Reconciliation/Reconciler.Test.cs | Updates existing reconciler tests to mock resolving IEnumerable<IEntityController<T>>. |
| test/KubeOps.Operator.Test/Reconciliation/Reconciler.MultiController.Test.cs | Adds tests covering multi-controller dispatch, ordering, and short-circuit behavior. |
| test/KubeOps.Operator.Test/Reconciliation/LabelSelectorMatcher.Test.cs | Adds unit tests for selector parsing/matching behavior. |
| test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs | Verifies multiple controller registrations and that watchers aren’t duplicated. |
| src/KubeOps.Operator/Reconciliation/Reconciler.cs | Replaces single-controller resolution with dispatch to all matching controllers. |
| src/KubeOps.Operator/Reconciliation/LabelSelectorMatcher.cs | Adds selector-expression matcher for KubeOps’ set-based selector syntax. |
| src/KubeOps.Operator/Builder/OperatorBuilder.cs | Switches controller DI registration from TryAddScoped to AddScoped to allow multiples. |
| src/KubeOps.Abstractions/Reconciliation/Controller/IEntityController{TEntity}.cs | Adds LabelFilter default interface property and documentation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| where TEntity : IKubernetesObject<V1ObjectMeta> | ||
| { | ||
| /// <summary> | ||
| /// An optional Kubernetes label selector expression (e.g. <c>app in (foo,bar),env=prod</c>) that |
There was a problem hiding this comment.
The XML doc example for LabelFilter includes env=prod, but the runtime matcher added in this PR only understands the set-based syntax emitted by KubeOps.KubernetesClient.LabelSelectors (e.g. env in (prod) / env notin (...), key, !key). As written, env=prod would never match and could lead to controllers silently not being invoked. Please either update the example/wording to the supported syntax, or extend LabelSelectorMatcher to also handle the standard Kubernetes key=value / key!=value forms.
| /// An optional Kubernetes label selector expression (e.g. <c>app in (foo,bar),env=prod</c>) that | |
| /// An optional Kubernetes label selector expression (e.g. <c>app in (foo,bar),env in (prod)</c>) that |
Multi-controller dispatch via
LabelFilteronIEntityController<TEntity>Fixes #909. A continuation of #911
Overview
Adds support for registering multiple controllers for the same Kubernetes entity type, each handling a different subset of resources based on label selectors. The implementation follows the approach requested in review: a single property on the existing interface, runtime dispatch, and no breaking changes.
Changes
IEntityController<TEntity>— one new default propertyControllers that need to scope themselves declare a selector expression:
OperatorBuilder—AddScopedinstead ofTryAddScopedOne line change.
TryAddScopedsilently drops duplicate service registrations;AddScopedappends them, enablingGetServices<IEntityController<TEntity>>()to resolve all registered controllers for an entity type.Reconciler<TEntity>— dispatch to all matching controllersNew
DispatchToMatchingControllersmethod replaces the singleGetRequiredServicecall. It resolves all registered controllers, filters byLabelFilteragainst the entity's labels, and calls each matching one in registration order. On the first failure the chain short-circuits.LabelSelectorMatcher— new internal utilityEvaluates KubeOps set-based selector expressions (
key in (v1,v2),key notin (v1,v2),key,!key) against an entity's label dictionary at runtime. Handles multi-clause AND semantics and ignores commas inside parentheses.What was explicitly NOT changed
IEntityLabelSelector<TEntity>— untouchedResourceWatcher<TEntity>— untouched; one watcher per entity type regardless of controller countAddController<TImplementation, TEntity>()signature — unchangedTests
LabelSelectorMatcher.Test.cs— 22 unit tests covering all 4 selector operators, multi-clause AND, inner-comma handling, empty/null labelsReconciler.MultiController.Test.cs— 12 tests: catch-all null filter, label match/no-match, ordering, failure short-circuit, entity mutation pass-through,DeletedAsyncpathOperatorBuilder.Test.cs— 3 new tests: multiple registrations in service collection, runtimeGetServicesresolution, no duplicateResourceWatcherregisteredReconciler.Test.csmock: updated fromGetService(IEntityController<T>)toGetService(IEnumerable<IEntityController<T>>)to match the new dispatch pathExample
Backward compatibility
A controller with no
LabelFilteroverride returnsnull, which matches every entity — identical to existing behaviour. No existing code requires changes.