Skip to content

feat(controllers): Enable multiple controllers with different label selectors for the same entity type (V2)#1070

Open
stevefan1999-personal wants to merge 2 commits intodotnet:mainfrom
stevefan1999-personal:feat/multi-controller-label-filter
Open

feat(controllers): Enable multiple controllers with different label selectors for the same entity type (V2)#1070
stevefan1999-personal wants to merge 2 commits intodotnet:mainfrom
stevefan1999-personal:feat/multi-controller-label-filter

Conversation

@stevefan1999-personal
Copy link
Copy Markdown
Contributor

Multi-controller dispatch via LabelFilter on IEntityController<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 property

// Default: null = catch-all (matches all entities, preserves existing behaviour)
string? LabelFilter { get; } => null;

Controllers that need to scope themselves declare a selector expression:

public string? LabelFilter =>
    new LabelSelector[] { new EqualsSelector("env", "prod") }.ToExpression();

OperatorBuilderAddScoped instead of TryAddScoped

One line change. TryAddScoped silently drops duplicate service registrations; AddScoped appends them, enabling GetServices<IEntityController<TEntity>>() to resolve all registered controllers for an entity type.

Reconciler<TEntity> — dispatch to all matching controllers

New DispatchToMatchingControllers method replaces the single GetRequiredService call. It resolves all registered controllers, filters by LabelFilter against the entity's labels, and calls each matching one in registration order. On the first failure the chain short-circuits.

LabelSelectorMatcher — new internal utility

Evaluates 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> — untouched
  • ResourceWatcher<TEntity> — untouched; one watcher per entity type regardless of controller count
  • AddController<TImplementation, TEntity>() signature — unchanged
  • All existing controller implementations — work without modification

Tests

  • LabelSelectorMatcher.Test.cs — 22 unit tests covering all 4 selector operators, multi-clause AND, inner-comma handling, empty/null labels
  • Reconciler.MultiController.Test.cs — 12 tests: catch-all null filter, label match/no-match, ordering, failure short-circuit, entity mutation pass-through, DeletedAsync path
  • OperatorBuilder.Test.cs — 3 new tests: multiple registrations in service collection, runtime GetServices resolution, no duplicate ResourceWatcher registered
  • Fixed existing Reconciler.Test.cs mock: updated from GetService(IEntityController<T>) to GetService(IEnumerable<IEntityController<T>>) to match the new dispatch path

Example

// Register multiple controllers for the same entity type
builder.AddController<ProdController, MyEntity>();   // LabelFilter => "env in (prod)"
builder.AddController<DevController, MyEntity>();    // LabelFilter => "env in (dev)"
builder.AddController<AuditController, MyEntity>();  // LabelFilter => null (all entities)

Backward compatibility

A controller with no LabelFilter override returns null, which matches every entity — identical to existing behaviour. No existing code requires changes.

Copilot AI review requested due to automatic review settings March 30, 2026 13:41
@stevefan1999-personal
Copy link
Copy Markdown
Contributor Author

@dotnet-policy-service agree

@dotnet-policy-service agree

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 LabelFilter as a defaulted property on IEntityController<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 a LabelSelectorMatcher utility 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
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
/// 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

Copilot uses AI. Check for mistakes.
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.

[feature/bug]: Multiple entity controllers with different label selectors

2 participants