diff --git a/docs/docs/operator/building-blocks/controllers.mdx b/docs/docs/operator/building-blocks/controllers.mdx index 1aba0b3b..fe0cb7e7 100644 --- a/docs/docs/operator/building-blocks/controllers.mdx +++ b/docs/docs/operator/building-blocks/controllers.mdx @@ -31,6 +31,95 @@ public class V1DemoEntityController( } ``` +## Label Selectors + +Label selectors allow you to filter which resources your controller watches based on Kubernetes labels. This is useful when you want a controller to only process resources with specific labels. + +### Basic Label Selector + +By default, controllers use the `DefaultEntityLabelSelector` which doesn't apply any filtering (watches all resources of the specified type). + +### Custom Label Selector + +To create a custom label selector, implement the `IEntityLabelSelector` interface: + +```csharp +public class V1DemoEntityLabelSelector : IEntityLabelSelector +{ + public ValueTask GetLabelSelectorAsync(CancellationToken cancellationToken) + { + // Return a Kubernetes label selector string + return ValueTask.FromResult("app=demo,environment=production"); + } +} +``` + +### Implementing a Controller with Label Selector + +You can implement a controller that directly uses a custom label selector by implementing the `IEntityController` interface: + +```csharp +public class V1DemoEntityController : IEntityController +{ + private readonly ILogger _logger; + private readonly IKubernetesClient _client; + + public V1DemoEntityController( + ILogger logger, + IKubernetesClient client) + { + _logger = logger; + _client = client; + } + + public async Task ReconcileAsync(V1DemoEntity entity, CancellationToken token) + { + _logger.LogInformation("Reconciling entity {Entity} with custom label selector.", entity); + // Implement your reconciliation logic here + } + + public async Task DeletedAsync(V1DemoEntity entity, CancellationToken token) + { + _logger.LogInformation("Deleting entity {Entity} with custom label selector.", entity); + // Implement your cleanup logic here + } +} +``` + +### Using Label Selectors with Controllers + +There are multiple ways to register a controller with a label selector: + +#### New Style (Recommended) + +For controllers that implement `IEntityController` as shown above: + +```csharp +// In your Startup.cs or Program.cs +services.AddKubernetesOperator() + .AddController(); +``` + +#### Backward Compatibility + +By default, controllers that implement the simpler `IEntityController` interface use `DefaultEntityLabelSelector` behind the scenes, which doesn't apply any filtering. + +However, you can still use a custom label selector with these controllers: + +```csharp +// Controller implementation using the simpler interface +public class V1DemoEntityController : IEntityController +{ + // Implementation details... +} + +// In your Startup.cs or Program.cs +services.AddKubernetesOperator() + .AddController(null); +``` + +The null parameter is used for method overload disambiguation. This approach allows you to use a custom label selector with controllers that implement the simpler interface, providing backward compatibility. + ## Resource Watcher When you create a controller, KubeOps automatically creates a resource watcher (informer) for your entity type. This watcher: diff --git a/src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs b/src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs index 24e5db3c..469ba8e0 100644 --- a/src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs +++ b/src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs @@ -40,9 +40,24 @@ IOperatorBuilder AddController() /// Label Selector type. /// The builder for chaining. IOperatorBuilder AddController() + where TImplementation : class, IEntityController + where TEntity : IKubernetesObject + where TLabelSelector : class, IEntityLabelSelector; + + /// + /// Add a controller implementation for a specific entity to the operator with backward compatibility. + /// This overload allows controllers that implement IEntityController<TEntity> to be registered + /// with a specific label selector for backward compatibility. + /// + /// Implementation type of the controller. + /// Entity type. + /// Label Selector type. + /// Unused parameter for method overload disambiguation. + /// The builder for chaining. + IOperatorBuilder AddController(TImplementation? _ = null) where TImplementation : class, IEntityController where TEntity : IKubernetesObject - where TLabelSelector : class, IEntityLabelSelector; + where TLabelSelector : class, IEntityLabelSelector; /// /// Add a finalizer implementation for a specific entity. @@ -76,3 +91,4 @@ IOperatorBuilder AddFinalizer(string identifier) /// The builder for chaining. IOperatorBuilder AddCrdInstaller(Action? configure = null); } + diff --git a/src/KubeOps.Abstractions/Controller/IEntityController{TEntity}.cs b/src/KubeOps.Abstractions/Controller/IEntityController{TEntity}.cs index 87856087..1c30c858 100644 --- a/src/KubeOps.Abstractions/Controller/IEntityController{TEntity}.cs +++ b/src/KubeOps.Abstractions/Controller/IEntityController{TEntity}.cs @@ -1,17 +1,19 @@ using k8s; using k8s.Models; +using KubeOps.Abstractions.Entities; namespace KubeOps.Abstractions.Controller; /// /// Generic entity controller. The controller manages the reconcile loop -/// for a given entity type. +/// for a given entity type with a selector. /// /// The type of the Kubernetes entity. +/// The type of the label selector for the entity. /// /// Simple example controller that just logs the entity. /// -/// public class V1TestEntityController : IEntityController<V1TestEntity> +/// public class V1TestEntityController : IEntityController<V1TestEntity, DefaultEntityLabelSelector<V1TestEntity>> /// { /// private readonly ILogger<V1TestEntityController> _logger; /// @@ -33,8 +35,9 @@ namespace KubeOps.Abstractions.Controller; /// } /// /// -public interface IEntityController +public interface IEntityController where TEntity : IKubernetesObject + where TSelector : class, IEntityLabelSelector { /// /// Called for `added` and `modified` events from the watcher. @@ -54,3 +57,37 @@ public interface IEntityController /// Task DeletedAsync(TEntity entity, CancellationToken cancellationToken); } + +/// +/// Generic entity controller. The controller manages the reconcile loop +/// for a given entity type. This is a compatibility interface that inherits +/// from the new two-parameter interface with a default selector. +/// +/// The type of the Kubernetes entity. +/// +/// Simple example controller that just logs the entity. +/// +/// public class V1TestEntityController : IEntityController<V1TestEntity> +/// { +/// private readonly ILogger<V1TestEntityController> _logger; +/// +/// public V1TestEntityController( +/// ILogger<V1TestEntityController> logger) +/// { +/// _logger = logger; +/// } +/// +/// public async Task ReconcileAsync(V1TestEntity entity, CancellationToken token) +/// { +/// _logger.LogInformation("Reconciling entity {Entity}.", entity); +/// } +/// +/// public async Task DeletedAsync(V1TestEntity entity, CancellationToken token) +/// { +/// _logger.LogInformation("Deleting entity {Entity}.", entity); +/// } +/// } +/// +/// +public interface IEntityController : IEntityController> + where TEntity : IKubernetesObject; diff --git a/src/KubeOps.Abstractions/Entities/DefaultEntityLabelSelector{TEntity}.cs b/src/KubeOps.Abstractions/Entities/DefaultEntityLabelSelector{TEntity}.cs index e03abe07..dc42303a 100644 --- a/src/KubeOps.Abstractions/Entities/DefaultEntityLabelSelector{TEntity}.cs +++ b/src/KubeOps.Abstractions/Entities/DefaultEntityLabelSelector{TEntity}.cs @@ -3,7 +3,7 @@ namespace KubeOps.Abstractions.Entities; -public class DefaultEntityLabelSelector : IEntityLabelSelector +public class DefaultEntityLabelSelector : IEntityLabelSelector> where TEntity : IKubernetesObject { public ValueTask GetLabelSelectorAsync(CancellationToken cancellationToken) => ValueTask.FromResult(null); diff --git a/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs b/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs index c8e0d0a8..b634b256 100644 --- a/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs +++ b/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs @@ -7,9 +7,13 @@ namespace KubeOps.Abstractions.Entities; // An alternative would be to use a KeyedSingleton when registering this however that's only valid from .NET 8 and above. // Other methods are far less elegant #pragma warning disable S2326 -public interface IEntityLabelSelector +public interface IEntityLabelSelector where TEntity : IKubernetesObject { ValueTask GetLabelSelectorAsync(CancellationToken cancellationToken); } + +public interface IEntityLabelSelector : IEntityLabelSelector> + where TEntity : IKubernetesObject; + #pragma warning restore S2326 diff --git a/src/KubeOps.Operator/Builder/OperatorBuilder.cs b/src/KubeOps.Operator/Builder/OperatorBuilder.cs index f1dc4381..6e5a2e78 100644 --- a/src/KubeOps.Operator/Builder/OperatorBuilder.cs +++ b/src/KubeOps.Operator/Builder/OperatorBuilder.cs @@ -38,50 +38,66 @@ public OperatorBuilder(IServiceCollection services, OperatorSettings settings) public IOperatorBuilder AddController() where TImplementation : class, IEntityController + where TEntity : IKubernetesObject => + AddController>(); + + public IOperatorBuilder AddController() + where TImplementation : class, IEntityController where TEntity : IKubernetesObject + where TLabelSelector : class, IEntityLabelSelector { - Services.AddHostedService>(); - Services.TryAddScoped, TImplementation>(); + Services.AddHostedService>(); + + // Register the implementation for the two-parameter interface + Services.TryAddScoped, TImplementation>(); Services.TryAddSingleton(new TimedEntityQueue()); Services.TryAddTransient(); - Services.TryAddTransient>(services => - services.GetRequiredService().Create()); + Services.TryAddTransient>( + services => services.GetRequiredService().Create() + ); + Services.TryAddSingleton, TLabelSelector>(); if (_settings.EnableLeaderElection) { - Services.AddHostedService>(); + Services.AddHostedService>(); } else { - Services.AddHostedService>(); + Services.AddHostedService>(); } return this; } - public IOperatorBuilder AddController() + // Overload for controllers that implement IEntityController but are being registered with a specific label selector + // This allows backward compatibility for users who call AddController() + // even when their controller only implements IEntityController + public IOperatorBuilder AddController(TImplementation? _ = null) where TImplementation : class, IEntityController where TEntity : IKubernetesObject - where TLabelSelector : class, IEntityLabelSelector + where TLabelSelector : class, IEntityLabelSelector { - Services.AddHostedService>(); - Services.TryAddScoped, TImplementation>(); - Services.TryAddSingleton(new TimedEntityQueue()); - Services.TryAddTransient(); - Services.TryAddTransient>(services => - services.GetRequiredService().Create()); - Services.TryAddSingleton, TLabelSelector>(); - - if (_settings.EnableLeaderElection) + // Check if TImplementation actually implements IEntityController + if (typeof(IEntityController).IsAssignableFrom(typeof(TImplementation))) { - Services.AddHostedService>(); + // If it does, call the main method + return AddController(); } - else + + // If the controller only implements IEntityController, we can only register it with DefaultEntityLabelSelector + // We cannot support arbitrary label selectors with controllers that don't implement the two-parameter interface + if (typeof(TLabelSelector) != typeof(DefaultEntityLabelSelector)) { - Services.AddHostedService>(); + throw new InvalidOperationException( + $"Controller {typeof(TImplementation).Name} only implements IEntityController<{typeof(TEntity).Name}> " + + $"and cannot be used with label selector {typeof(TLabelSelector).Name}. " + + $"Either implement IEntityController<{typeof(TEntity).Name}, {typeof(TLabelSelector).Name}> " + + $"or use AddController<{typeof(TImplementation).Name}, {typeof(TEntity).Name}>() instead." + ); } - return this; + // If TLabelSelector is DefaultEntityLabelSelector, delegate to the two-parameter method + return AddController(); } public IOperatorBuilder AddFinalizer(string identifier) @@ -90,9 +106,12 @@ public IOperatorBuilder AddFinalizer(string identifier { Services.TryAddKeyedTransient, TImplementation>(identifier); Services.TryAddTransient(); - Services.TryAddTransient>(services => - services.GetRequiredService() - .Create(identifier)); + Services.TryAddTransient>( + services => + services + .GetRequiredService() + .Create(identifier) + ); return this; } @@ -131,7 +150,9 @@ private void AddOperatorBase() Services.TryAddTransient(services => services.GetRequiredService().Create()); - Services.AddSingleton(typeof(IEntityLabelSelector<>), typeof(DefaultEntityLabelSelector<>)); + // Register default entity label selector for all entities + // Note: We cannot register the open generic types directly due to arity mismatch + // The registration happens in AddController when specific types are needed if (_settings.EnableLeaderElection) { diff --git a/src/KubeOps.Operator/Queue/EntityRequeueBackgroundService.cs b/src/KubeOps.Operator/Queue/EntityRequeueBackgroundService.cs index dcfff6c9..7285f9af 100644 --- a/src/KubeOps.Operator/Queue/EntityRequeueBackgroundService.cs +++ b/src/KubeOps.Operator/Queue/EntityRequeueBackgroundService.cs @@ -2,6 +2,7 @@ using k8s.Models; using KubeOps.Abstractions.Controller; +using KubeOps.Abstractions.Entities; using KubeOps.KubernetesClient; using Microsoft.Extensions.DependencyInjection; @@ -10,12 +11,13 @@ namespace KubeOps.Operator.Queue; -internal sealed class EntityRequeueBackgroundService( +internal sealed class EntityRequeueBackgroundService( IKubernetesClient client, TimedEntityQueue queue, IServiceProvider provider, - ILogger> logger) : IHostedService, IDisposable, IAsyncDisposable + ILogger> logger) : IHostedService, IDisposable, IAsyncDisposable where TEntity : IKubernetesObject + where TSelector : class, IEntityLabelSelector { private readonly CancellationTokenSource _cts = new(); private bool _disposed; @@ -119,7 +121,7 @@ private async Task ReconcileSingleAsync(TEntity queued, CancellationToken cancel } await using var scope = provider.CreateAsyncScope(); - var controller = scope.ServiceProvider.GetRequiredService>(); + var controller = scope.ServiceProvider.GetRequiredService>(); await controller.ReconcileAsync(entity, cancellationToken); } } diff --git a/src/KubeOps.Operator/Watcher/LeaderAwareResourceWatcher{TEntity}.cs b/src/KubeOps.Operator/Watcher/LeaderAwareResourceWatcher{TEntity}.cs index add231f7..5e1f68e5 100644 --- a/src/KubeOps.Operator/Watcher/LeaderAwareResourceWatcher{TEntity}.cs +++ b/src/KubeOps.Operator/Watcher/LeaderAwareResourceWatcher{TEntity}.cs @@ -20,18 +20,36 @@ internal sealed class LeaderAwareResourceWatcher( IServiceProvider provider, TimedEntityQueue queue, OperatorSettings settings, - IEntityLabelSelector labelSelector, + IEntityLabelSelector> labelSelector, IKubernetesClient client, IHostApplicationLifetime hostApplicationLifetime, - LeaderElector elector) - : ResourceWatcher( + LeaderElector elector +) + : LeaderAwareResourceWatcher>( activitySource, logger, provider, queue, settings, labelSelector, - client) + client, + hostApplicationLifetime, + elector + ) + where TEntity : IKubernetesObject; + +internal class LeaderAwareResourceWatcher( + ActivitySource activitySource, + ILogger> logger, + IServiceProvider provider, + TimedEntityQueue queue, + OperatorSettings settings, + IEntityLabelSelector labelSelector, + IKubernetesClient client, + IHostApplicationLifetime hostApplicationLifetime, + LeaderElector elector +) : ResourceWatcher(activitySource, logger, provider, queue, settings, labelSelector, client) + where TSelector : class, IEntityLabelSelector where TEntity : IKubernetesObject { private CancellationTokenSource _cts = new(); diff --git a/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs b/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs index 8d74ba6b..67d85d19 100644 --- a/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs +++ b/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs @@ -23,13 +23,34 @@ namespace KubeOps.Operator.Watcher; public class ResourceWatcher( ActivitySource activitySource, - ILogger> logger, + ILogger>> logger, IServiceProvider provider, TimedEntityQueue requeue, OperatorSettings settings, - IEntityLabelSelector labelSelector, - IKubernetesClient client) - : IHostedService, IAsyncDisposable, IDisposable + IEntityLabelSelector> labelSelector, + IKubernetesClient client +) + : ResourceWatcher>( + activitySource, + logger, + provider, + requeue, + settings, + labelSelector, + client + ) + where TEntity : IKubernetesObject; + +public class ResourceWatcher( + ActivitySource activitySource, + ILogger> logger, + IServiceProvider provider, + TimedEntityQueue requeue, + OperatorSettings settings, + IEntityLabelSelector labelSelector, + IKubernetesClient client +) : IHostedService, IAsyncDisposable, IDisposable + where TSelector : class, IEntityLabelSelector where TEntity : IKubernetesObject { private readonly ConcurrentDictionary _entityCache = new(); @@ -314,7 +335,7 @@ private async Task ReconcileDeletionAsync(TEntity entity, CancellationToken canc _entityCache.TryRemove(entity.Uid(), out _); await using var scope = provider.CreateAsyncScope(); - var controller = scope.ServiceProvider.GetRequiredService>(); + var controller = scope.ServiceProvider.GetRequiredService>(); await controller.DeletedAsync(entity, cancellationToken); } @@ -355,7 +376,7 @@ private async Task ReconcileModificationAsync(TEntity entity, CancellationToken // Re-queue should requested in the controller reconcile method. Invalidate any existing queues. requeue.Remove(entity); await using var scope = provider.CreateAsyncScope(); - var controller = scope.ServiceProvider.GetRequiredService>(); + var controller = scope.ServiceProvider.GetRequiredService>(); await controller.ReconcileAsync(entity, cancellationToken); } } diff --git a/test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs b/test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs index c8b514c0..28687c0b 100644 --- a/test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs +++ b/test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs @@ -6,6 +6,7 @@ using KubeOps.Abstractions.Events; using KubeOps.Abstractions.Finalizer; using KubeOps.Abstractions.Queue; +using KubeOps.KubernetesClient; using KubeOps.KubernetesClient.LabelSelectors; using KubeOps.Operator.Builder; using KubeOps.Operator.Finalizer; @@ -26,6 +27,11 @@ public class OperatorBuilderTest [Fact] public void Should_Add_Default_Resources() { + // Add controllers to trigger the label selector registrations + _builder.AddController, V1OperatorIntegrationTestEntity, TestLabelSelector>(); + _builder.AddController, V1OperatorIntegrationTestEntity, TestLabelSelector2>(); + + // This test verifies the basic services that are registered _builder.Services.Should().Contain(s => s.ServiceType == typeof(OperatorSettings) && s.Lifetime == ServiceLifetime.Singleton); @@ -33,8 +39,18 @@ public void Should_Add_Default_Resources() s.ServiceType == typeof(EventPublisher) && s.Lifetime == ServiceLifetime.Transient); _builder.Services.Should().Contain(s => - s.ServiceType == typeof(IEntityLabelSelector<>) && - s.ImplementationType == typeof(DefaultEntityLabelSelector<>) && + s.ServiceType == typeof(IEventPublisherFactory) && + s.Lifetime == ServiceLifetime.Transient); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(IKubernetesClient) && + s.Lifetime == ServiceLifetime.Singleton); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(IEntityLabelSelector) && + s.ImplementationType == typeof(TestLabelSelector) && + s.Lifetime == ServiceLifetime.Singleton); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(IEntityLabelSelector) && + s.ImplementationType == typeof(TestLabelSelector2) && s.Lifetime == ServiceLifetime.Singleton); } @@ -45,16 +61,27 @@ public void Should_Use_Specific_EntityLabelSelector_Implementation() var services = new ServiceCollection(); // Register the default and specific implementations - services.AddSingleton(typeof(IEntityLabelSelector<>), typeof(DefaultEntityLabelSelector<>)); - services.TryAddSingleton, TestLabelSelector>(); + services.TryAddSingleton, TestLabelSelector>(); + services.TryAddSingleton, TestLabelSelector2>(); + var serviceProvider = services.BuildServiceProvider(); - // Act - var resolvedService = serviceProvider.GetRequiredService>(); + { + // Act + var resolvedService = serviceProvider.GetRequiredService>(); - // Assert - Assert.IsType(resolvedService); + // Assert + Assert.IsType(resolvedService); + } + + { + // Act + var resolvedService = serviceProvider.GetRequiredService>(); + + // Assert + Assert.IsType(resolvedService); + } } [Fact] @@ -63,12 +90,12 @@ public void Should_Add_Controller_Resources() _builder.AddController(); _builder.Services.Should().Contain(s => - s.ServiceType == typeof(IEntityController) && + s.ServiceType == typeof(IEntityController>) && s.ImplementationType == typeof(TestController) && s.Lifetime == ServiceLifetime.Scoped); _builder.Services.Should().Contain(s => s.ServiceType == typeof(IHostedService) && - s.ImplementationType == typeof(ResourceWatcher) && + s.ImplementationType == typeof(ResourceWatcher>) && s.Lifetime == ServiceLifetime.Singleton); _builder.Services.Should().Contain(s => s.ServiceType == typeof(TimedEntityQueue) && @@ -81,15 +108,24 @@ public void Should_Add_Controller_Resources() [Fact] public void Should_Add_Controller_Resources_With_Label_Selector() { - _builder.AddController(); + _builder.AddController, V1OperatorIntegrationTestEntity, TestLabelSelector>(); + _builder.AddController, V1OperatorIntegrationTestEntity, TestLabelSelector2>(); _builder.Services.Should().Contain(s => - s.ServiceType == typeof(IEntityController) && - s.ImplementationType == typeof(TestController) && + s.ServiceType == typeof(IEntityController) && + s.ImplementationType == typeof(TestControllerWithSelector) && s.Lifetime == ServiceLifetime.Scoped); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(IEntityController) && + s.ImplementationType == typeof(TestControllerWithSelector) && + s.Lifetime == ServiceLifetime.Scoped); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(IHostedService) && + s.ImplementationType == typeof(ResourceWatcher) && + s.Lifetime == ServiceLifetime.Singleton); _builder.Services.Should().Contain(s => s.ServiceType == typeof(IHostedService) && - s.ImplementationType == typeof(ResourceWatcher) && + s.ImplementationType == typeof(ResourceWatcher) && s.Lifetime == ServiceLifetime.Singleton); _builder.Services.Should().Contain(s => s.ServiceType == typeof(TimedEntityQueue) && @@ -98,9 +134,13 @@ public void Should_Add_Controller_Resources_With_Label_Selector() s.ServiceType == typeof(EntityRequeue) && s.Lifetime == ServiceLifetime.Transient); _builder.Services.Should().Contain(s => - s.ServiceType == typeof(IEntityLabelSelector) && + s.ServiceType == typeof(IEntityLabelSelector) && s.ImplementationType == typeof(TestLabelSelector) && s.Lifetime == ServiceLifetime.Singleton); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(IEntityLabelSelector) && + s.ImplementationType == typeof(TestLabelSelector2) && + s.Lifetime == ServiceLifetime.Singleton); } [Fact] @@ -134,11 +174,11 @@ public void Should_Add_LeaderAwareResourceWatcher() builder.Services.Should().Contain(s => s.ServiceType == typeof(IHostedService) && - s.ImplementationType == typeof(LeaderAwareResourceWatcher) && + s.ImplementationType == typeof(LeaderAwareResourceWatcher>) && s.Lifetime == ServiceLifetime.Singleton); builder.Services.Should().NotContain(s => s.ServiceType == typeof(IHostedService) && - s.ImplementationType == typeof(ResourceWatcher) && + s.ImplementationType == typeof(ResourceWatcher>) && s.Lifetime == ServiceLifetime.Singleton); } @@ -151,13 +191,23 @@ public Task DeletedAsync(V1OperatorIntegrationTestEntity entity, CancellationTok Task.CompletedTask; } + private class TestControllerWithSelector : IEntityController + where TSelector : class, IEntityLabelSelector + { + public Task ReconcileAsync(V1OperatorIntegrationTestEntity entity, CancellationToken cancellationToken) => + Task.CompletedTask; + + public Task DeletedAsync(V1OperatorIntegrationTestEntity entity, CancellationToken cancellationToken) => + Task.CompletedTask; + } + private class TestFinalizer : IEntityFinalizer { public Task FinalizeAsync(V1OperatorIntegrationTestEntity entity, CancellationToken cancellationToken) => Task.CompletedTask; } - private class TestLabelSelector : IEntityLabelSelector + private class TestLabelSelector : IEntityLabelSelector { public ValueTask GetLabelSelectorAsync(CancellationToken cancellationToken) { @@ -169,4 +219,18 @@ private class TestLabelSelector : IEntityLabelSelector(labelSelectors.ToExpression()); } } + + private class TestLabelSelector2 : IEntityLabelSelector + { + public ValueTask GetLabelSelectorAsync(CancellationToken cancellationToken) + { + var labelSelectors = new LabelSelector[] + { + new EqualsSelector("label", "value"), + new EqualsSelector("label2", "value") + }; + + return ValueTask.FromResult(labelSelectors.ToExpression()); + } + } }