diff --git a/_old/src/KubeOps/Operator/Entities/Extensions/KubernetesObjectExtensions.cs b/_old/src/KubeOps/Operator/Entities/Extensions/KubernetesObjectExtensions.cs index 1812f2f6..88257c67 100644 --- a/_old/src/KubeOps/Operator/Entities/Extensions/KubernetesObjectExtensions.cs +++ b/_old/src/KubeOps/Operator/Entities/Extensions/KubernetesObjectExtensions.cs @@ -36,21 +36,7 @@ public static V1OwnerReference MakeOwnerReference(this IKubernetesObject - /// Create a of a kubernetes object. - /// - /// The object that should be translated. - /// The created . - public static V1ObjectReference MakeObjectReference(this IKubernetesObject kubernetesObject) - => new() - { - ApiVersion = kubernetesObject.ApiVersion, - Kind = kubernetesObject.Kind, - Name = kubernetesObject.Metadata.Name, - NamespaceProperty = kubernetesObject.Metadata.NamespaceProperty, - ResourceVersion = kubernetesObject.Metadata.ResourceVersion, - Uid = kubernetesObject.Metadata.Uid, - }; + private static IList EnsureOwnerReferences(this V1ObjectMeta meta) => meta.OwnerReferences ??= new List(); diff --git a/_old/src/KubeOps/Operator/Events/EventManager.cs b/_old/src/KubeOps/Operator/Events/EventManager.cs deleted file mode 100644 index 54a0d4e2..00000000 --- a/_old/src/KubeOps/Operator/Events/EventManager.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Security.Cryptography; -using System.Text; - -using k8s; -using k8s.Models; - -using KubeOps.KubernetesClient; -using KubeOps.Operator.Entities.Extensions; - -using SimpleBase; - -namespace KubeOps.Operator.Events; - -internal class EventManager : IEventManager -{ - private readonly IKubernetesClient _client; - private readonly OperatorSettings _settings; - private readonly ILogger _logger; - - public EventManager(IKubernetesClient client, OperatorSettings settings, ILogger logger) - { - _client = client; - _settings = settings; - _logger = logger; - } - - public async Task PublishAsync( - IKubernetesObject resource, - string reason, - string message, - EventType type = EventType.Normal) - { - var resourceNamespace = resource.Namespace() ?? "default"; - - _logger.LogTrace( - "Encoding event name with: {resourceName}.{resourceNamespace}.{reason}.{message}.{type}.", - resource.Name(), - resourceNamespace, - reason, - message, - type); - var eventName = - Base32.Rfc4648.Encode( - SHA512.HashData( - Encoding.UTF8.GetBytes($"{resource.Name()}.{resourceNamespace}.{reason}.{message}.{type}"))); - _logger.LogTrace(@"Search or create event with name ""{name}"".", eventName); - var @event = await _client.Get(eventName, resourceNamespace) ?? - new Corev1Event - { - Kind = Corev1Event.KubeKind, - ApiVersion = $"{Corev1Event.KubeGroup}/{Corev1Event.KubeApiVersion}", - Metadata = new() - { - Name = eventName, - NamespaceProperty = resourceNamespace, - Annotations = new Dictionary - { - { "nameHash", "sha512" }, { "nameEncoding", "Base32 / RFC 4648" }, - }, - }, - Type = type.ToString(), - Reason = reason, - Message = message, - ReportingComponent = _settings.Name, - ReportingInstance = Environment.MachineName, - Source = new() { Component = _settings.Name, }, - InvolvedObject = resource.MakeObjectReference(), - FirstTimestamp = DateTime.UtcNow, - LastTimestamp = DateTime.UtcNow, - Count = 0, - }; - - @event.Count++; - @event.LastTimestamp = DateTime.UtcNow; - _logger.LogTrace( - "Save event with new count {count} and last timestamp {timestamp}", - @event.Count, - @event.LastTimestamp); - - try - { - await _client.Save(@event); - _logger.LogInformation( - @"Created or updated event with name ""{name}"" to new count {count} on resource ""{kind}/{name}"".", - eventName, - @event.Count, - resource.Kind, - resource.Name()); - } - catch (Exception e) - { - _logger.LogError( - e, - @"Could not publish event with name ""{name}"" on resource ""{kind}/{name}"".", - eventName, - resource.Kind, - resource.Name()); - } - } - - public Task PublishAsync(Corev1Event @event) - => _client.Save(@event); - - public IEventManager.AsyncPublisher CreatePublisher( - string reason, - string message, - EventType type = EventType.Normal) - => resource => PublishAsync(resource, reason, message, type); - - public IEventManager.AsyncStaticPublisher CreatePublisher( - IKubernetesObject resource, - string reason, - string message, - EventType type = EventType.Normal) - => () => PublishAsync(resource, reason, message, type); - - public IEventManager.AsyncMessagePublisher CreatePublisher(string reason, EventType type = EventType.Normal) - => (resource, message) => PublishAsync(resource, reason, message, type); -} diff --git a/_old/src/KubeOps/Operator/Events/IEventManager.cs b/_old/src/KubeOps/Operator/Events/IEventManager.cs deleted file mode 100644 index fac18967..00000000 --- a/_old/src/KubeOps/Operator/Events/IEventManager.cs +++ /dev/null @@ -1,105 +0,0 @@ -using k8s; -using k8s.Models; - -namespace KubeOps.Operator.Events; - -/// -/// Event manager for objects. -/// Contains various utility methods for emitting events on objects. -/// -public interface IEventManager -{ - /// - /// Delegate that publishes a predefined event for a statically given resource. - /// This delegate should be created with - /// . - /// When called, the publisher creates or updates the event defined by the params. - /// - /// A task that completes when the event is published. - public delegate Task AsyncStaticPublisher(); - - /// - /// Delegate that publishes a predefined event for a resource that is passed. - /// This delegate should be created with - /// . - /// When called with a resource, the publisher creates or updates the event defined by the params. - /// - /// The resource on which the event should be published. - /// A task that completes when the event is published. - public delegate Task AsyncPublisher(IKubernetesObject resource); - - /// - /// Delegate that publishes a given message to a predefined reason/severity. - /// This delegate should be created with - /// When called with the resource and a message, the publisher creates or updates the event - /// given by the params. - /// - /// The resource on which the event should be published. - /// The message that the event should contain. - /// A task that completes when the event is published. - public delegate Task AsyncMessagePublisher(IKubernetesObject resource, string message); - - /// - /// PublishAsync an event in relation to a given resource. - /// The event is created or updated if it exists. - /// - /// The resource that is involved with the event. - /// The reason string. This should be a machine readable reason string. - /// A human readable string for the event. - /// The type of the event. - /// A task that finishes when the event is created or updated. - Task PublishAsync( - IKubernetesObject resource, - string reason, - string message, - EventType type = EventType.Normal); - - /// - /// Create or update an event. - /// - /// The full event object that should be created or updated. - /// A task that finishes when the event is created or updated. - Task PublishAsync(Corev1Event @event); - - /// - /// Create a for a predefined event. - /// The is then called with a resource (). - /// The predefined event is published with this resource as the involved object. - /// - /// The reason string. This should be a machine readable reason string. - /// A human readable string for the event. - /// The type of the event. - /// A delegate that can be called to create or update events. - AsyncPublisher CreatePublisher( - string reason, - string message, - EventType type = EventType.Normal); - - /// - /// Create a for a predefined event. - /// The is then called without any parameters. - /// The predefined event is published with the initially given resource as the involved object. - /// - /// The resource that is involved with the event. - /// The reason string. This should be a machine readable reason string. - /// A human readable string for the event. - /// The type of the event. - /// A delegate that can be called to create or update events. - AsyncStaticPublisher CreatePublisher( - IKubernetesObject resource, - string reason, - string message, - EventType type = EventType.Normal); - - /// - /// Create a for a predefined reason and severity. - /// The can then be called with a "message" and - /// a resource to create/update the event. - /// - /// The predefined reason (machine readable reason) for the event. - /// The type of the event. - /// A delegate that can be called to create or update events. - AsyncMessagePublisher CreatePublisher( - string reason, - EventType type = EventType.Normal); -} diff --git a/_old/src/KubeOps/Operator/OperatorSettings.cs b/_old/src/KubeOps/Operator/OperatorSettings.cs deleted file mode 100644 index 76163eca..00000000 --- a/_old/src/KubeOps/Operator/OperatorSettings.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System.Reflection; -using System.Text.RegularExpressions; - -using KellermanSoftware.CompareNetObjects; - -using KubeOps.KubernetesClient; -using KubeOps.Operator.Errors; - -using static System.Net.WebRequestMethods; - -namespace KubeOps.Operator; - -/// -/// Operator settings. -/// -public sealed class OperatorSettings -{ - private const string DefaultOperatorName = "KubernetesOperator"; - private const string NonCharReplacement = "-"; - - /// - /// The name of the operator that appears in logs and other elements. - /// - public string Name { get; set; } = - new Regex(@"(\W|_)", RegexOptions.CultureInvariant).Replace( - Assembly.GetEntryAssembly()?.GetName().Name ?? DefaultOperatorName, - NonCharReplacement) - .ToLowerInvariant(); - - /// - /// - /// Controls the namespace which is watched by the operator. - /// If this field is left `null`, all namespaces are watched for - /// CRD instances. - /// - /// - /// The namespace could be passed to the software via environment - /// variable or can be fetched via the - /// method of the . - /// - /// - public string? Namespace { get; set; } - - /// - /// The maximal number of retries for an error during reconciliation. - /// The controller skips the reconciliation event if an entity throws errors during - /// reconciliation. Depending on the , this could - /// result in endless waiting times. - /// - public int MaxErrorRetries { get; set; } = 4; - - /// - /// The maximal number of seconds that the resource watcher waits until it retries to connect to Kubernetes. - /// The amount of time is determined by and the minimal value of - /// the calculated one and this configuration is used. - /// - public int WatcherMaxRetrySeconds { get; set; } = 32; - - /// - /// Configures the for error events during reconciliation. - /// When the controller faces an error, it waits for the returned amount of time of this strategy - /// and retries until the controller drops the event configured by . - /// - public BackoffStrategy ErrorBackoffStrategy { get; set; } = BackoffStrategies.ExponentialBackoffStrategy; - - /// - /// The http endpoint, where the metrics are exposed. - /// - public string MetricsEndpoint { get; set; } = "/metrics"; - - /// - /// The http endpoint, where the liveness probes are exposed. - /// - public string LivenessEndpoint { get; set; } = "/health"; - - /// - /// The http endpoint, where the readiness probes are exposed. - /// - public string ReadinessEndpoint { get; set; } = "/ready"; - - /// - /// - /// Defines if the leader elector should run. You may disable this, - /// if you don't intend to run your operator multiple times. - /// - /// - /// If this is disabled, and an operator runs in multiple instance - /// (in the same namespace) it can lead to a "split brain" problem. - /// - /// - /// This could be disabled when developing locally. - /// - /// - public bool EnableLeaderElection { get; set; } = true; - - /// - /// - /// If set to true, controllers will only watch for new events when in a leader state, - /// or if leadership is disabled. When false, this check is disabled, - /// controllers will always watch for resource changes regardless of leadership state. - /// - /// - /// If this is disabled, you should consider checking leadership state manually, - /// to prevent a "split brain" problem. - /// - /// - /// Defaults to true. - /// - /// - public bool OnlyWatchEventsWhenLeader { get; set; } = true; - - /// - /// The interval in seconds in which this particular instance of the operator - /// will check for leader election. - /// - public ushort LeaderElectionCheckInterval { get; set; } = 15; - - /// - /// The duration in seconds in which the leader lease is valid. - /// - public ushort LeaderElectionLeaseDuration { get; set; } = 30; - - /// - /// The timeout in seconds which the watcher has (after this timeout, the server will close the connection). - /// - public ushort WatcherHttpTimeout { get; set; } = 60; - - /// - /// - /// If set to true, controllers perform a search for already - /// existing objects in the cluster and load them into the objects cache. - /// - /// - /// This bears the risk of not catching elements when they are created - /// during downtime of the operator. - /// - /// The search will be performed on each "Start" of the controller. - /// - public bool PreloadCache { get; set; } - - /// - /// - /// If set to true, returning `ResourceControllerResult.RequeueEvent` will - /// automatically requeue the event as the same type. - /// - /// - /// For example, if done from a "Created" event, the event will be queued - /// again as "Created" instead of (for example) "NotModified". - /// - /// - public bool DefaultRequeueAsSameType { get; set; } = false; - - /// - /// - /// If set to true, the executing assembly will be scanned for controllers, - /// finalizers, mutators and validators. - /// - /// - public bool EnableAssemblyScanning { get; set; } = true; - - /// - /// The configured http port that the operator should run - /// on Kubernetes. This has no direct impact on the startup call in `Program.cs`, - /// but on the generated yaml files of the operator. This setting modifies - /// the environment variable "KESTREL__ENDPOINTS__HTTP__URL" in the yaml file. - /// - public short HttpPort { get; set; } = 5000; - - /// - /// The configured https port that the operator should run - /// on Kubernetes. This has no direct impact on the startup call in `Program.cs`, - /// but on the generated yaml files of the operator. This setting modifies - /// the environment variable "KESTREL__ENDPOINTS__HTTPS__URL" in the yaml file. - /// - public short HttpsPort { get; set; } = 5001; - - /// - /// The configuration used when comparing resources against each-other for caching - /// or other similar processing. - /// - public ComparisonConfig CacheComparisonConfig { get; set; } = new() - { - Caching = true, - AutoClearCache = false, - MembersToIgnore = new List { "ResourceVersion", "ManagedFields" }, - }; - - /// - /// Use this to configure the operator to execute any custom code before any of the installers or execution begins. - /// - /// The issue 567 removed the downloading of the cloudflare certificate tooling and moved it to the dockerfile. This is more secure, but may not be suitable for all users. Use this action to reproduce the logic from before and download the appropriate tooling. - /// - /// - public Action? PreInitializeAction { get; set; } -} diff --git a/_old/src/KubeOps/README.md b/_old/src/KubeOps/README.md index 47c062c8..9bf9d68b 100644 --- a/_old/src/KubeOps/README.md +++ b/_old/src/KubeOps/README.md @@ -540,6 +540,7 @@ The event manager allows you to either publish an event that you created by yourself, or helps you publish events with predefined data. If you want to use the helper: + ```c# // fetch from DI, or inject into your controller. IEventManager manager = services.GetRequiredService; @@ -553,6 +554,7 @@ await manager.PublishAsync(resource, "reason", "my fancy message"); ``` If you want full control over the event: + ```c# // fetch from DI, or inject into your controller. IEventManager manager = services.GetRequiredService; @@ -573,12 +575,14 @@ If you don't want to call the `KubeOps.Operator.Events.IEventManager.PublishAsyn all the time with the same arguments, you can create delegates. There exist two different delegates: + - "AsyncStaticPublisher": Predefined event on a predefined resource. - "AsyncPublisher": Predefined event on a variable resource. To use the static publisher: + ```c# var publisher = manager.CreatePublisher(resource, "reason", "message"); await publisher(); @@ -588,6 +592,7 @@ await publisher(); // again without specifying reason / message and so on. ``` To use the dynamic publisher: + ```c# var publisher = manager.CreatePublisher("reason", "message"); await publisher(resource); @@ -599,6 +604,7 @@ await publisher(resource); // again without specifying reason / message and so o The dynamic publisher can be used to predefine the event for your resources. As an example in a controller: + ```c# public class TestController : IResourceController { @@ -1065,26 +1071,26 @@ The props file just defines the defaults. You can overwrite the default behaviour of the building parts with the following variables that you can add in a `` in your `csproj` file: -| Property | Description | Default Value | -| ---------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| KubeOpsBasePath | Base path for all other elements | `$(MSBuildProjectDirectory)` | -| KubeOpsDockerfilePath | The path of the dockerfile | `$(KubeOpsBasePath)\Dockerfile` | -| KubeOpsDockerTag | Which dotnet sdk / run tag should be used | `latest` | -| KubeOpsConfigRoot | The base directory for generated elements | `$(KubeOpsBasePath)\config` | -| KubeOpsCrdDir | The directory for the generated crds | `$(KubeOpsConfigRoot)\crds` | -| KubeOpsCrdFormat | Output format for crds | `Yaml` | -| KubeOpsCrdUseOldCrds | Use V1Beta version of crd instead of V1
(for kubernetes version < 1.16) | `false` | -| KubeOpsRbacDir | Where to put the roles | `$(KubeOpsConfigRoot)\rbac` | -| KubeOpsRbacFormat | Output format for rbac | `Yaml` | -| KubeOpsOperatorDir | Where to put operator related elements
(e.g. Deployment) | `$(KubeOpsConfigRoot)\operator` | -| KubeOpsOperatorFormat | Output format for the operator | `Yaml` | -| KubeOpsInstallerDir | Where to put the installation files
(e.g. Namespace / Kustomization) | `$(KubeOpsConfigRoot)\install` | -| KubeOpsInstallerFormat | Output format for the installation files | `Yaml` | -| KubeOpsSkipDockerfile | Skip dockerfile during build | `""` | -| KubeOpsSkipCrds | Skip crd generation during build | `""` | -| KubeOpsSkipRbac | Skip rbac generation during build | `""` | -| KubeOpsSkipOperator | Skip operator generation during build | `""` | -| KubeOpsSkipInstaller | Skip installer generation during build | `""` | +| Property | Description | Default Value | +| ---------------------- | -------------------------------------------------------------------------- | ------------------------------- | +| KubeOpsBasePath | Base path for all other elements | `$(MSBuildProjectDirectory)` | +| KubeOpsDockerfilePath | The path of the dockerfile | `$(KubeOpsBasePath)\Dockerfile` | +| KubeOpsDockerTag | Which dotnet sdk / run tag should be used | `latest` | +| KubeOpsConfigRoot | The base directory for generated elements | `$(KubeOpsBasePath)\config` | +| KubeOpsCrdDir | The directory for the generated crds | `$(KubeOpsConfigRoot)\crds` | +| KubeOpsCrdFormat | Output format for crds | `Yaml` | +| KubeOpsCrdUseOldCrds | Use V1Beta version of crd instead of V1
(for kubernetes version < 1.16) | `false` | +| KubeOpsRbacDir | Where to put the roles | `$(KubeOpsConfigRoot)\rbac` | +| KubeOpsRbacFormat | Output format for rbac | `Yaml` | +| KubeOpsOperatorDir | Where to put operator related elements
(e.g. Deployment) | `$(KubeOpsConfigRoot)\operator` | +| KubeOpsOperatorFormat | Output format for the operator | `Yaml` | +| KubeOpsInstallerDir | Where to put the installation files
(e.g. Namespace / Kustomization) | `$(KubeOpsConfigRoot)\install` | +| KubeOpsInstallerFormat | Output format for the installation files | `Yaml` | +| KubeOpsSkipDockerfile | Skip dockerfile during build | `""` | +| KubeOpsSkipCrds | Skip crd generation during build | `""` | +| KubeOpsSkipRbac | Skip rbac generation during build | `""` | +| KubeOpsSkipOperator | Skip operator generation during build | `""` | +| KubeOpsSkipInstaller | Skip installer generation during build | `""` | ## Advanced Topics diff --git a/examples/Operator/Controller/V1TestEntityController.cs b/examples/Operator/Controller/V1TestEntityController.cs index 086e3763..7f6960f2 100644 --- a/examples/Operator/Controller/V1TestEntityController.cs +++ b/examples/Operator/Controller/V1TestEntityController.cs @@ -1,4 +1,5 @@ using KubeOps.Abstractions.Controller; +using KubeOps.Abstractions.Events; using KubeOps.Abstractions.Finalizer; using KubeOps.Abstractions.Queue; using KubeOps.Abstractions.Rbac; @@ -17,23 +18,25 @@ public class V1TestEntityController : IEntityController private readonly ILogger _logger; private readonly IKubernetesClient _client; private readonly EntityRequeue _requeue; + private readonly EventPublisher _eventPublisher; public V1TestEntityController( ILogger logger, IKubernetesClient client, - EntityRequeue requeue) + EntityRequeue requeue, + EventPublisher eventPublisher) { _logger = logger; _client = client; _requeue = requeue; + _eventPublisher = eventPublisher; } public async Task ReconcileAsync(V1TestEntity entity) { _logger.LogInformation("Reconciling entity {Entity}.", entity); - entity.Status.Status = "Reconciled"; - await _client.UpdateStatusAsync(entity); + await _eventPublisher(entity, "RECONCILED", "Entity was reconciled."); _requeue(entity, TimeSpan.FromSeconds(5)); } diff --git a/examples/Operator/Program.cs b/examples/Operator/Program.cs index d001455d..0bd007a9 100644 --- a/examples/Operator/Program.cs +++ b/examples/Operator/Program.cs @@ -1,4 +1,4 @@ -using KubeOps.Operator.Extensions; +using KubeOps.Operator; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/examples/Operator/todos.txt b/examples/Operator/todos.txt index 7d149c06..0f2f3cda 100644 --- a/examples/Operator/todos.txt +++ b/examples/Operator/todos.txt @@ -1,10 +1,8 @@ todo: -- events - leadership election - build targets - other CLI commands - error handling - web: webhooks - docs -- cache? - try .net 8 AOT? diff --git a/src/KubeOps.Abstractions/Builder/OperatorSettings.cs b/src/KubeOps.Abstractions/Builder/OperatorSettings.cs new file mode 100644 index 00000000..ea8fb153 --- /dev/null +++ b/src/KubeOps.Abstractions/Builder/OperatorSettings.cs @@ -0,0 +1,57 @@ +using System.Text.RegularExpressions; + +namespace KubeOps.Abstractions.Builder; + +/// +/// Operator settings. +/// +public sealed class OperatorSettings +{ + private const string DefaultOperatorName = "KubernetesOperator"; + private const string NonCharReplacement = "-"; + + /// + /// The name of the operator that appears in logs and other elements. + /// Defaults to "kubernetesoperator" when not set. + /// + public string Name { get; set; } = + new Regex(@"(\W|_)", RegexOptions.CultureInvariant).Replace( + DefaultOperatorName, + NonCharReplacement) + .ToLowerInvariant(); + + /// + /// + /// Controls the namespace which is watched by the operator. + /// If this field is left `null`, all namespaces are watched for + /// CRD instances. + /// + /// + public string? Namespace { get; set; } + + /// + /// + /// Defines if the leader elector should run. You may disable this, + /// if you don't intend to run your operator multiple times. + /// + /// + /// If this is disabled, and an operator runs in multiple instance + /// (in the same namespace) it can lead to a "split brain" problem. + /// + /// + /// This could be disabled when developing locally. + /// + /// + public bool EnableLeaderElection { get; set; } = true; + + /// + /// The interval in seconds in which this particular instance of the operator + /// will check for leader election. + /// + public ushort LeaderElectionCheckInterval { get; set; } = 15; + + /// + /// The duration in seconds in which the leader lease is valid. + /// + public ushort LeaderElectionLeaseDuration { get; set; } = 30; +} diff --git a/src/KubeOps.Abstractions/Entities/Extensions.cs b/src/KubeOps.Abstractions/Entities/Extensions.cs index 1a5a9b36..085f9cca 100644 --- a/src/KubeOps.Abstractions/Entities/Extensions.cs +++ b/src/KubeOps.Abstractions/Entities/Extensions.cs @@ -39,4 +39,20 @@ public static TEntity WithResourceVersion( entity.EnsureMetadata().ResourceVersion = other.ResourceVersion(); return entity; } + + /// + /// Create a of a kubernetes object. + /// + /// The object that should be translated. + /// The created . + public static V1ObjectReference MakeObjectReference(this IKubernetesObject kubernetesObject) + => new() + { + ApiVersion = kubernetesObject.ApiVersion, + Kind = kubernetesObject.Kind, + Name = kubernetesObject.Metadata.Name, + NamespaceProperty = kubernetesObject.Metadata.NamespaceProperty, + ResourceVersion = kubernetesObject.Metadata.ResourceVersion, + Uid = kubernetesObject.Metadata.Uid, + }; } diff --git a/_old/src/KubeOps/Operator/Events/EventType.cs b/src/KubeOps.Abstractions/Events/EventType.cs similarity index 87% rename from _old/src/KubeOps/Operator/Events/EventType.cs rename to src/KubeOps.Abstractions/Events/EventType.cs index 75237f8c..68183ca0 100644 --- a/_old/src/KubeOps/Operator/Events/EventType.cs +++ b/src/KubeOps.Abstractions/Events/EventType.cs @@ -1,20 +1,20 @@ -using k8s.Models; - -namespace KubeOps.Operator.Events; - -/// -/// The type of a . -/// The event type will be stringified and used as . -/// -public enum EventType -{ - /// - /// A normal event, informative value. - /// - Normal, - - /// - /// A warning, something might went wrong. - /// - Warning, -} +using k8s.Models; + +namespace KubeOps.Abstractions.Events; + +/// +/// The type of a . +/// The event type will be stringified and used as . +/// +public enum EventType +{ + /// + /// A normal event, informative value. + /// + Normal, + + /// + /// A warning, something might went wrong. + /// + Warning, +} diff --git a/src/KubeOps.Abstractions/Events/Publisher.cs b/src/KubeOps.Abstractions/Events/Publisher.cs new file mode 100644 index 00000000..cef72725 --- /dev/null +++ b/src/KubeOps.Abstractions/Events/Publisher.cs @@ -0,0 +1,46 @@ +using k8s; +using k8s.Models; + +namespace KubeOps.Abstractions.Events; + +/// +/// This injectable delegate publishes events on entities. Events are created in the same +/// namespace as the provided entity. However, if no namespace is provided (for example in +/// cluster wide entities), the "default" namespace is used. +/// +/// The delegate creates a if none does exist or updates the +/// count and last seen timestamp if the same event already fired. +/// +/// Events have a hex encoded name of a SHA512 hash. For the delegate to update +/// an event, the entity, reason, message, and type must be the same. +/// +/// The entity that is involved with the event. +/// The reason string. This should be a machine readable reason string. +/// A human readable string for the event. +/// The of the event (either normal or warning). +/// A task that finishes when the event is created or updated. +/// +/// Controller that fires a simple reconcile event on any entity it encounters. +/// Note that the publication of an event does not trigger another reconcile. +/// +/// public class V1TestEntityController : IEntityController<V1TestEntity> +/// { +/// private readonly EventPublisher _eventPublisher; +/// +/// public V1TestEntityController() +/// { +/// _eventPublisher = eventPublisher; +/// } +/// +/// public async Task ReconcileAsync(V1TestEntity entity) +/// { +/// await _eventPublisher(entity, "Reconciled", "Entity was reconciled."); +/// } +/// } +/// +/// +public delegate Task EventPublisher( + IKubernetesObject entity, + string reason, + string message, + EventType type = EventType.Normal); diff --git a/src/KubeOps.Abstractions/Queue/EntityRequeue.cs b/src/KubeOps.Abstractions/Queue/EntityRequeue.cs index 17a45fe4..036e4ae5 100644 --- a/src/KubeOps.Abstractions/Queue/EntityRequeue.cs +++ b/src/KubeOps.Abstractions/Queue/EntityRequeue.cs @@ -1,47 +1,47 @@ -using k8s; -using k8s.Models; - -namespace KubeOps.Abstractions.Queue; - -/// -/// Injectable delegate for requeueing entities. -/// -/// Use this delegate when you need to pro-actively reconcile an entity after a -/// certain amount of time. This is useful if you want to check your entities -/// periodically. -/// -/// -/// After the timeout is reached, the entity is fetched -/// from the API and passed to the controller for reconciliation. -/// If the entity was deleted in the meantime, the controller will not be called. -/// -/// -/// If the entity gets modified while the timeout is running, the timer -/// is canceled and restarted, if another requeue is requested. -/// -/// -/// The type of the entity. -/// The instance of the entity that should be requeued. -/// The time to wait before another reconcile loop is fired. -/// -/// Use the requeue delegate to repeatedly reconcile an entity after 5 seconds. -/// -/// [EntityRbac(typeof(V1TestEntity), Verbs = RbacVerb.All)] -/// public class V1TestEntityController : IEntityController<V1TestEntity> -/// { -/// private readonly EntityRequeue<V1TestEntity> _requeue; -/// -/// public V1TestEntityController(EntityRequeue<V1TestEntity> requeue) -/// { -/// _requeue = requeue; -/// } -/// -/// public async Task ReconcileAsync(V1TestEntity entity) -/// { -/// _requeue(entity, TimeSpan.FromSeconds(5)); -/// } -/// } -/// -/// -public delegate void EntityRequeue(TEntity entity, TimeSpan requeueIn) - where TEntity : IKubernetesObject; +using k8s; +using k8s.Models; + +namespace KubeOps.Abstractions.Queue; + +/// +/// Injectable delegate for requeueing entities. +/// +/// Use this delegate when you need to pro-actively reconcile an entity after a +/// certain amount of time. This is useful if you want to check your entities +/// periodically. +/// +/// +/// After the timeout is reached, the entity is fetched +/// from the API and passed to the controller for reconciliation. +/// If the entity was deleted in the meantime, the controller will not be called. +/// +/// +/// If the entity gets modified while the timeout is running, the timer +/// is canceled and restarted, if another requeue is requested. +/// +/// +/// The type of the entity. +/// The instance of the entity that should be requeued. +/// The time to wait before another reconcile loop is fired. +/// +/// Use the requeue delegate to repeatedly reconcile an entity after 5 seconds. +/// +/// [EntityRbac(typeof(V1TestEntity), Verbs = RbacVerb.All)] +/// public class V1TestEntityController : IEntityController<V1TestEntity> +/// { +/// private readonly EntityRequeue<V1TestEntity> _requeue; +/// +/// public V1TestEntityController(EntityRequeue<V1TestEntity> requeue) +/// { +/// _requeue = requeue; +/// } +/// +/// public async Task ReconcileAsync(V1TestEntity entity) +/// { +/// _requeue(entity, TimeSpan.FromSeconds(5)); +/// } +/// } +/// +/// +public delegate void EntityRequeue(TEntity entity, TimeSpan requeueIn) + where TEntity : IKubernetesObject; diff --git a/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs b/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs index d1b1fbdf..524d0571 100644 --- a/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs +++ b/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs @@ -1,137 +1,137 @@ -using System.Text; - -using KubeOps.Generator.SyntaxReceiver; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; - -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace KubeOps.Generator.Generators; - -[Generator] -internal class EntityInitializerGenerator : ISourceGenerator -{ - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new KubernetesEntitySyntaxReceiver()); - } - - public void Execute(GeneratorExecutionContext context) - { - if (context.SyntaxContextReceiver is not KubernetesEntitySyntaxReceiver receiver) - { - return; - } - - // for each partial defined entity, create a partial class that - // introduces a default constructor that initializes the ApiVersion and Kind. - // But only, if there is no default constructor defined. - foreach (var entity in receiver.Entities - .Where(e => e.Class.Modifiers.Any(SyntaxKind.PartialKeyword)) - .Where(e => !e.Class.Members.Any(m => m is ConstructorDeclarationSyntax - { - ParameterList.Parameters.Count: 0, - }))) - { - var symbol = context.Compilation - .GetSemanticModel(entity.Class.SyntaxTree) - .GetDeclaredSymbol(entity.Class)!; - - var ns = new List(); - if (!symbol.ContainingNamespace.IsGlobalNamespace) - { - ns.Add(FileScopedNamespaceDeclaration(IdentifierName(symbol.ContainingNamespace.ToDisplayString()))); - } - - var partialEntityInitializer = CompilationUnit() - .AddMembers(ns.ToArray()) - .AddMembers(ClassDeclaration(entity.Class.Identifier) - .WithModifiers(entity.Class.Modifiers) - .AddMembers(ConstructorDeclaration(entity.Class.Identifier) - .WithModifiers( - TokenList( - Token(SyntaxKind.PublicKeyword))) - .WithBody( - Block( - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - IdentifierName("ApiVersion"), - LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal($"{entity.Group}/{entity.Version}".TrimStart('/'))))), - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - IdentifierName("Kind"), - LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal(entity.Kind)))))))) - .NormalizeWhitespace(); - - context.AddSource( - $"{entity.Class.Identifier}.init.g.cs", - SourceText.From(partialEntityInitializer.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); - } - - // for each NON partial entity, generate a method extension that initializes the ApiVersion and Kind. - var staticInitializers = CompilationUnit() - .WithMembers(SingletonList(ClassDeclaration("EntityInitializer") - .WithModifiers(TokenList( - Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) - .WithMembers(List(receiver.Entities - .Where(e => !e.Class.Modifiers.Any(SyntaxKind.PartialKeyword) || e.Class.Members.Any(m => - m is ConstructorDeclarationSyntax - { - ParameterList.Parameters.Count: 0, - })) - .Select(e => (Entity: e, - ClassIdentifier: context.Compilation.GetSemanticModel(e.Class.SyntaxTree) - .GetDeclaredSymbol(e.Class)!.ToDisplayString(SymbolDisplayFormat - .FullyQualifiedFormat))) - .Select(e => - MethodDeclaration( - IdentifierName(e.ClassIdentifier), - "Initialize") - .WithModifiers( - TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) - .WithParameterList(ParameterList( - SingletonSeparatedList( - Parameter( - Identifier("entity")) - .WithModifiers( - TokenList( - Token(SyntaxKind.ThisKeyword))) - .WithType(IdentifierName(e.ClassIdentifier))))) - .WithBody(Block( - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("entity"), - IdentifierName("ApiVersion")), - LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal($"{e.Entity.Group}/{e.Entity.Version}".TrimStart('/'))))), - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("entity"), - IdentifierName("Kind")), - LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal(e.Entity.Kind)))), - ReturnStatement(IdentifierName("entity"))))))))) - .NormalizeWhitespace(); - - context.AddSource( - "EntityInitializer.g.cs", - SourceText.From(staticInitializers.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); - } -} +using System.Text; + +using KubeOps.Generator.SyntaxReceiver; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace KubeOps.Generator.Generators; + +[Generator] +internal class EntityInitializerGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new KubernetesEntitySyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxContextReceiver is not KubernetesEntitySyntaxReceiver receiver) + { + return; + } + + // for each partial defined entity, create a partial class that + // introduces a default constructor that initializes the ApiVersion and Kind. + // But only, if there is no default constructor defined. + foreach (var entity in receiver.Entities + .Where(e => e.Class.Modifiers.Any(SyntaxKind.PartialKeyword)) + .Where(e => !e.Class.Members.Any(m => m is ConstructorDeclarationSyntax + { + ParameterList.Parameters.Count: 0, + }))) + { + var symbol = context.Compilation + .GetSemanticModel(entity.Class.SyntaxTree) + .GetDeclaredSymbol(entity.Class)!; + + var ns = new List(); + if (!symbol.ContainingNamespace.IsGlobalNamespace) + { + ns.Add(FileScopedNamespaceDeclaration(IdentifierName(symbol.ContainingNamespace.ToDisplayString()))); + } + + var partialEntityInitializer = CompilationUnit() + .AddMembers(ns.ToArray()) + .AddMembers(ClassDeclaration(entity.Class.Identifier) + .WithModifiers(entity.Class.Modifiers) + .AddMembers(ConstructorDeclaration(entity.Class.Identifier) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword))) + .WithBody( + Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("ApiVersion"), + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal($"{entity.Group}/{entity.Version}".TrimStart('/'))))), + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("Kind"), + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(entity.Kind)))))))) + .NormalizeWhitespace(); + + context.AddSource( + $"{entity.Class.Identifier}.init.g.cs", + SourceText.From(partialEntityInitializer.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); + } + + // for each NON partial entity, generate a method extension that initializes the ApiVersion and Kind. + var staticInitializers = CompilationUnit() + .WithMembers(SingletonList(ClassDeclaration("EntityInitializer") + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) + .WithMembers(List(receiver.Entities + .Where(e => !e.Class.Modifiers.Any(SyntaxKind.PartialKeyword) || e.Class.Members.Any(m => + m is ConstructorDeclarationSyntax + { + ParameterList.Parameters.Count: 0, + })) + .Select(e => (Entity: e, + ClassIdentifier: context.Compilation.GetSemanticModel(e.Class.SyntaxTree) + .GetDeclaredSymbol(e.Class)!.ToDisplayString(SymbolDisplayFormat + .FullyQualifiedFormat))) + .Select(e => + MethodDeclaration( + IdentifierName(e.ClassIdentifier), + "Initialize") + .WithModifiers( + TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) + .WithParameterList(ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("entity")) + .WithModifiers( + TokenList( + Token(SyntaxKind.ThisKeyword))) + .WithType(IdentifierName(e.ClassIdentifier))))) + .WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("entity"), + IdentifierName("ApiVersion")), + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal($"{e.Entity.Group}/{e.Entity.Version}".TrimStart('/'))))), + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("entity"), + IdentifierName("Kind")), + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(e.Entity.Kind)))), + ReturnStatement(IdentifierName("entity"))))))))) + .NormalizeWhitespace(); + + context.AddSource( + "EntityInitializer.g.cs", + SourceText.From(staticInitializers.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); + } +} diff --git a/src/KubeOps.Operator/Builder/OperatorBuilder.cs b/src/KubeOps.Operator/Builder/OperatorBuilder.cs index 01c1a164..3f3c3b46 100644 --- a/src/KubeOps.Operator/Builder/OperatorBuilder.cs +++ b/src/KubeOps.Operator/Builder/OperatorBuilder.cs @@ -1,9 +1,13 @@ -using k8s; +using System.Security.Cryptography; +using System.Text; + +using k8s; using k8s.Models; using KubeOps.Abstractions.Builder; using KubeOps.Abstractions.Controller; using KubeOps.Abstractions.Entities; +using KubeOps.Abstractions.Events; using KubeOps.Abstractions.Finalizer; using KubeOps.Abstractions.Queue; using KubeOps.KubernetesClient; @@ -18,9 +22,13 @@ namespace KubeOps.Operator.Builder; internal class OperatorBuilder : IOperatorBuilder { - public OperatorBuilder(IServiceCollection services) + public OperatorBuilder(IServiceCollection services, OperatorSettings settings) { Services = services; + Services.AddSingleton(settings); + Services.AddTransient>(_ => new KubernetesClient(new( + Corev1Event.KubeKind, Corev1Event.KubeApiVersion, Plural: Corev1Event.KubePluralName))); + Services.AddTransient(CreateEventPublisher()); } public IServiceCollection Services { get; } @@ -39,19 +47,7 @@ public IOperatorBuilder AddController() Services.AddScoped, TImplementation>(); Services.AddHostedService>(); Services.AddSingleton(new TimedEntityQueue()); - Services.AddTransient>(services => (entity, timespan) => - { - var logger = services.GetService>>(); - var queue = services.GetRequiredService>(); - - logger?.LogTrace( - """Requeue entity "{kind}/{name}" in {milliseconds}ms.""", - entity.Kind, - entity.Name(), - timespan.TotalMilliseconds); - - queue.Enqueue(entity, timespan); - }); + Services.AddTransient(CreateEntityRequeue()); return this; } @@ -67,7 +63,17 @@ public IOperatorBuilder AddFinalizer(string identifier { Services.AddTransient(); Services.AddSingleton(new FinalizerRegistration(identifier, typeof(TImplementation), typeof(TEntity))); - Services.AddTransient>(services => async entity => + Services.AddTransient(CreateFinalizerAttacher(identifier)); + + return this; + } + + private static Func> CreateFinalizerAttacher< + TImplementation, TEntity>( + string identifier) + where TImplementation : class, IEntityFinalizer + where TEntity : IKubernetesObject + => services => async entity => { var logger = services.GetService>>(); var client = services.GetRequiredService>(); @@ -89,8 +95,101 @@ public IOperatorBuilder AddFinalizer(string identifier entity.Kind, entity.Name()); return await client.UpdateAsync(entity); - }); + }; - return this; - } + private static Func> CreateEntityRequeue() + where TEntity : IKubernetesObject + => services => (entity, timespan) => + { + var logger = services.GetService>>(); + var queue = services.GetRequiredService>(); + + logger?.LogTrace( + """Requeue entity "{kind}/{name}" in {milliseconds}ms.""", + entity.Kind, + entity.Name(), + timespan.TotalMilliseconds); + + queue.Enqueue(entity, timespan); + }; + + private static Func CreateEventPublisher() + => services => + async (entity, reason, message, type) => + { + var logger = services.GetService>(); + using var client = services.GetRequiredService>(); + var settings = services.GetRequiredService(); + + var @namespace = entity.Namespace() ?? "default"; + logger?.LogTrace( + "Encoding event name with: {resourceName}.{resourceNamespace}.{reason}.{message}.{type}.", + entity.Name(), + @namespace, + reason, + message, + type); + + var eventName = $"{entity.Name()}.{@namespace}.{reason}.{message}.{type}"; + var encodedEventName = + Convert.ToHexString( + SHA512.HashData( + Encoding.UTF8.GetBytes(eventName))); + + logger?.LogTrace("""Search or create event with name "{name}".""", encodedEventName); + + var @event = await client.GetAsync(encodedEventName, @namespace) ?? + new Corev1Event + { + Metadata = new() + { + Name = encodedEventName, + NamespaceProperty = @namespace, + Annotations = + new Dictionary + { + { "originalName", eventName }, + { "nameHash", "sha512" }, + { "nameEncoding", "Hex String" }, + }, + }, + Type = type.ToString(), + Reason = reason, + Message = message, + ReportingComponent = settings.Name, + ReportingInstance = Environment.MachineName, + Source = new() { Component = settings.Name, }, + InvolvedObject = entity.MakeObjectReference(), + FirstTimestamp = DateTime.UtcNow, + LastTimestamp = DateTime.UtcNow, + Count = 0, + }.Initialize(); + + @event.Count++; + @event.LastTimestamp = DateTime.UtcNow; + logger?.LogTrace( + "Save event with new count {count} and last timestamp {timestamp}", + @event.Count, + @event.LastTimestamp); + + try + { + await client.SaveAsync(@event); + logger?.LogInformation( + """Created or updated event with name "{name}" to new count {count} on entity "{kind}/{name}".""", + eventName, + @event.Count, + entity.Kind, + entity.Name()); + } + catch (Exception e) + { + logger?.LogError( + e, + """Could not publish event with name "{name}" on entity "{kind}/{name}".""", + eventName, + entity.Kind, + entity.Name()); + } + }; } diff --git a/src/KubeOps.Operator/Queue/TimedEntityQueue.cs b/src/KubeOps.Operator/Queue/TimedEntityQueue.cs index fe7d3e5b..d46f54b2 100644 --- a/src/KubeOps.Operator/Queue/TimedEntityQueue.cs +++ b/src/KubeOps.Operator/Queue/TimedEntityQueue.cs @@ -1,63 +1,63 @@ -using System.Collections.Concurrent; - -using k8s; -using k8s.Models; - -using Timer = System.Timers.Timer; - -namespace KubeOps.Operator.Queue; - -internal class TimedEntityQueue - where TEntity : IKubernetesObject -{ - private readonly ConcurrentDictionary _queue = new(); - - public event EventHandler<(string Name, string? Namespace)>? RequeueRequested; - - internal int Count => _queue.Count; - - public void Clear() - { - foreach (var (_, _, timer) in _queue.Values) - { - timer.Stop(); - } - - _queue.Clear(); - } - - public void Enqueue(TEntity entity, TimeSpan requeueIn) - { - var (_, _, timer) = - _queue.AddOrUpdate( - entity.Uid(), - (entity.Name(), entity.Namespace(), new Timer(requeueIn.TotalMilliseconds)), - (_, e) => - { - e.Timer.Stop(); - e.Timer.Dispose(); - return (e.Name, e.Namespace, new Timer(requeueIn.TotalMilliseconds)); - }); - - timer.Elapsed += (_, _) => - { - if (!_queue.TryRemove(entity.Metadata.Uid, out var e)) - { - return; - } - - e.Timer.Stop(); - e.Timer.Dispose(); - RequeueRequested?.Invoke(this, (e.Name, e.Namespace)); - }; - timer.Start(); - } - - public void RemoveIfQueued(TEntity entity) - { - if (_queue.TryRemove(entity.Uid(), out var entry)) - { - entry.Timer.Stop(); - } - } -} +using System.Collections.Concurrent; + +using k8s; +using k8s.Models; + +using Timer = System.Timers.Timer; + +namespace KubeOps.Operator.Queue; + +internal class TimedEntityQueue + where TEntity : IKubernetesObject +{ + private readonly ConcurrentDictionary _queue = new(); + + public event EventHandler<(string Name, string? Namespace)>? RequeueRequested; + + internal int Count => _queue.Count; + + public void Clear() + { + foreach (var (_, _, timer) in _queue.Values) + { + timer.Stop(); + } + + _queue.Clear(); + } + + public void Enqueue(TEntity entity, TimeSpan requeueIn) + { + var (_, _, timer) = + _queue.AddOrUpdate( + entity.Uid(), + (entity.Name(), entity.Namespace(), new Timer(requeueIn.TotalMilliseconds)), + (_, e) => + { + e.Timer.Stop(); + e.Timer.Dispose(); + return (e.Name, e.Namespace, new Timer(requeueIn.TotalMilliseconds)); + }); + + timer.Elapsed += (_, _) => + { + if (!_queue.TryRemove(entity.Metadata.Uid, out var e)) + { + return; + } + + e.Timer.Stop(); + e.Timer.Dispose(); + RequeueRequested?.Invoke(this, (e.Name, e.Namespace)); + }; + timer.Start(); + } + + public void RemoveIfQueued(TEntity entity) + { + if (_queue.TryRemove(entity.Uid(), out var entry)) + { + entry.Timer.Stop(); + } + } +} diff --git a/src/KubeOps.Operator/Extensions/ServiceCollectionExtensions.cs b/src/KubeOps.Operator/ServiceCollectionExtensions.cs similarity index 59% rename from src/KubeOps.Operator/Extensions/ServiceCollectionExtensions.cs rename to src/KubeOps.Operator/ServiceCollectionExtensions.cs index c8998b0d..c661e944 100644 --- a/src/KubeOps.Operator/Extensions/ServiceCollectionExtensions.cs +++ b/src/KubeOps.Operator/ServiceCollectionExtensions.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection; -namespace KubeOps.Operator.Extensions; +namespace KubeOps.Operator; /// /// Method extensions for the . @@ -14,7 +14,14 @@ public static class ServiceCollectionExtensions /// Add the Kubernetes operator to the dependency injection. /// /// . + /// An optional configure action for adjusting settings in the operator. /// An for further configuration and chaining. public static IOperatorBuilder AddKubernetesOperator( - this IServiceCollection services) => new OperatorBuilder(services); + this IServiceCollection services, + Action? configure = null) + { + var settings = new OperatorSettings(); + configure?.Invoke(settings); + return new OperatorBuilder(services, settings); + } } diff --git a/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs b/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs index ba3a0fd4..8744c3dd 100644 --- a/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs +++ b/src/KubeOps.Operator/Watcher/ResourceWatcher{TEntity}.cs @@ -210,7 +210,7 @@ private async Task ReconcileFinalizer(TEntity entity) var pendingFinalizer = entity.Finalizers(); if (_finalizers.Value.Find(reg => reg.EntityType == entity.GetType() && pendingFinalizer.Contains(reg.Identifier)) is not - { Identifier: var identifier, FinalizerType: var type }) + { Identifier: var identifier, FinalizerType: var type }) { _logger.LogDebug( """Entity "{kind}/{name}" is finalizing but this operator has no registered finalizers for it.""", diff --git a/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs b/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs index 23a33928..1776ff69 100644 --- a/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs +++ b/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs @@ -1,287 +1,287 @@ -using FluentAssertions; - -using KubeOps.Generator.Generators; - -using Microsoft.CodeAnalysis.CSharp; - -namespace KubeOps.Generator.Test; - -public class EntityInitializerGeneratorTest -{ - [Fact] - public void Should_Generate_Empty_Initializer_Without_Input() - { - var inputCompilation = string.Empty.CreateCompilation(); - var expectedResult = """ - public static class EntityInitializer - { - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } - - [Fact] - public void Should_Generate_Static_Initializer_For_Non_Partial_Entities() - { - var inputCompilation = """ - [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public class V1TestEntity : IKubernetesObject - { - } - - [KubernetesEntity(Group = "testing.dev", ApiVersion = "v2", Kind = "TestEntity")] - public class V2TestEntity : IKubernetesObject - { - } - """.CreateCompilation(); - var expectedResult = """ - public static class EntityInitializer - { - public static global::V1TestEntity Initialize(this global::V1TestEntity entity) - { - entity.ApiVersion = "testing.dev/v1"; - entity.Kind = "TestEntity"; - return entity; - } - - public static global::V2TestEntity Initialize(this global::V2TestEntity entity) - { - entity.ApiVersion = "testing.dev/v2"; - entity.Kind = "TestEntity"; - return entity; - } - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - output.SyntaxTrees.Any(s => s.FilePath.Contains("V1TestEntity")).Should().BeFalse(); - output.SyntaxTrees.Any(s => s.FilePath.Contains("V2TestEntity")).Should().BeFalse(); - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } - - [Fact] - public void Should_Generate_Correct_Initializer_Entities_Without_Groups() - { - var inputCompilation = """ - [KubernetesEntity(ApiVersion = "v1", Kind = "ConfigMap")] - public class V1ConfigMap : IKubernetesObject - { - } - """.CreateCompilation(); - var expectedResult = """ - public static class EntityInitializer - { - public static global::V1ConfigMap Initialize(this global::V1ConfigMap entity) - { - entity.ApiVersion = "v1"; - entity.Kind = "ConfigMap"; - return entity; - } - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - output.SyntaxTrees.Any(s => s.FilePath.Contains("V1ConfigMap")).Should().BeFalse(); - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } - - [Fact] - public void Should_Generate_Static_Initializer_For_Partial_Entity_With_Default_Ctor() - { - var inputCompilation = """ - [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public partial class V1TestEntity : IKubernetesObject - { - public V1TestEntity(){} - } - """.CreateCompilation(); - var expectedResult = """ - public static class EntityInitializer - { - public static global::V1TestEntity Initialize(this global::V1TestEntity entity) - { - entity.ApiVersion = "testing.dev/v1"; - entity.Kind = "TestEntity"; - return entity; - } - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - output.SyntaxTrees.Any(s => s.FilePath.Contains("V1TestEntity")).Should().BeFalse(); - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } - - [Fact] - public void Should_Not_Generate_Static_Initializer_For_Partial_Entity() - { - var inputCompilation = """ - [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public partial class V1TestEntity : IKubernetesObject - { - } - """.CreateCompilation(); - var expectedResult = """ - public static class EntityInitializer - { - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - output.SyntaxTrees.Any(s => s.FilePath.Contains("V1TestEntity")).Should().BeTrue(); - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } - - [Fact] - public void Should_Generate_Default_Ctor_For_FileNamespaced_Partial_Entity() - { - var inputCompilation = """ - namespace Foo.Bar; - [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public partial class V1TestEntity : IKubernetesObject - { - } - """.CreateCompilation(); - var expectedResult = """ - namespace Foo.Bar; - public partial class V1TestEntity - { - public V1TestEntity() - { - ApiVersion = "testing.dev/v1"; - Kind = "TestEntity"; - } - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } - - [Fact] - public void Should_Generate_Default_Ctor_For_ScopeNamespaced_Partial_Entity() - { - var inputCompilation = """ - namespace Foo.Bar - { - namespace Baz - { - [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public partial class V1TestEntity : IKubernetesObject - { - } - } - } - """.CreateCompilation(); - var expectedResult = """ - namespace Foo.Bar.Baz; - public partial class V1TestEntity - { - public V1TestEntity() - { - ApiVersion = "testing.dev/v1"; - Kind = "TestEntity"; - } - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } - - [Fact] - public void Should_Generate_Default_Ctor_For_Global_Partial_Entity() - { - var inputCompilation = """ - [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public partial class V1TestEntity : IKubernetesObject - { - } - """.CreateCompilation(); - var expectedResult = """ - public partial class V1TestEntity - { - public V1TestEntity() - { - ApiVersion = "testing.dev/v1"; - Kind = "TestEntity"; - } - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } - - [Fact] - public void Should_Generate_Default_Ctor_For_Partial_Entity_With_Ctor() - { - var inputCompilation = """ - [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] - public partial class V1TestEntity : IKubernetesObject - { - public V1TestEntity(string name){} - } - """.CreateCompilation(); - var expectedResult = """ - public partial class V1TestEntity - { - public V1TestEntity() - { - ApiVersion = "testing.dev/v1"; - Kind = "TestEntity"; - } - } - """.ReplaceLineEndings(); - - var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); - driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); - - var result = output.SyntaxTrees - .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) - .ToString().ReplaceLineEndings(); - result.Should().Be(expectedResult); - } -} +using FluentAssertions; + +using KubeOps.Generator.Generators; + +using Microsoft.CodeAnalysis.CSharp; + +namespace KubeOps.Generator.Test; + +public class EntityInitializerGeneratorTest +{ + [Fact] + public void Should_Generate_Empty_Initializer_Without_Input() + { + var inputCompilation = string.Empty.CreateCompilation(); + var expectedResult = """ + public static class EntityInitializer + { + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } + + [Fact] + public void Should_Generate_Static_Initializer_For_Non_Partial_Entities() + { + var inputCompilation = """ + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public class V1TestEntity : IKubernetesObject + { + } + + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v2", Kind = "TestEntity")] + public class V2TestEntity : IKubernetesObject + { + } + """.CreateCompilation(); + var expectedResult = """ + public static class EntityInitializer + { + public static global::V1TestEntity Initialize(this global::V1TestEntity entity) + { + entity.ApiVersion = "testing.dev/v1"; + entity.Kind = "TestEntity"; + return entity; + } + + public static global::V2TestEntity Initialize(this global::V2TestEntity entity) + { + entity.ApiVersion = "testing.dev/v2"; + entity.Kind = "TestEntity"; + return entity; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + output.SyntaxTrees.Any(s => s.FilePath.Contains("V1TestEntity")).Should().BeFalse(); + output.SyntaxTrees.Any(s => s.FilePath.Contains("V2TestEntity")).Should().BeFalse(); + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } + + [Fact] + public void Should_Generate_Correct_Initializer_Entities_Without_Groups() + { + var inputCompilation = """ + [KubernetesEntity(ApiVersion = "v1", Kind = "ConfigMap")] + public class V1ConfigMap : IKubernetesObject + { + } + """.CreateCompilation(); + var expectedResult = """ + public static class EntityInitializer + { + public static global::V1ConfigMap Initialize(this global::V1ConfigMap entity) + { + entity.ApiVersion = "v1"; + entity.Kind = "ConfigMap"; + return entity; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + output.SyntaxTrees.Any(s => s.FilePath.Contains("V1ConfigMap")).Should().BeFalse(); + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } + + [Fact] + public void Should_Generate_Static_Initializer_For_Partial_Entity_With_Default_Ctor() + { + var inputCompilation = """ + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public partial class V1TestEntity : IKubernetesObject + { + public V1TestEntity(){} + } + """.CreateCompilation(); + var expectedResult = """ + public static class EntityInitializer + { + public static global::V1TestEntity Initialize(this global::V1TestEntity entity) + { + entity.ApiVersion = "testing.dev/v1"; + entity.Kind = "TestEntity"; + return entity; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + output.SyntaxTrees.Any(s => s.FilePath.Contains("V1TestEntity")).Should().BeFalse(); + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } + + [Fact] + public void Should_Not_Generate_Static_Initializer_For_Partial_Entity() + { + var inputCompilation = """ + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public partial class V1TestEntity : IKubernetesObject + { + } + """.CreateCompilation(); + var expectedResult = """ + public static class EntityInitializer + { + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + output.SyntaxTrees.Any(s => s.FilePath.Contains("V1TestEntity")).Should().BeTrue(); + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("EntityInitializer.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } + + [Fact] + public void Should_Generate_Default_Ctor_For_FileNamespaced_Partial_Entity() + { + var inputCompilation = """ + namespace Foo.Bar; + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public partial class V1TestEntity : IKubernetesObject + { + } + """.CreateCompilation(); + var expectedResult = """ + namespace Foo.Bar; + public partial class V1TestEntity + { + public V1TestEntity() + { + ApiVersion = "testing.dev/v1"; + Kind = "TestEntity"; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } + + [Fact] + public void Should_Generate_Default_Ctor_For_ScopeNamespaced_Partial_Entity() + { + var inputCompilation = """ + namespace Foo.Bar + { + namespace Baz + { + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public partial class V1TestEntity : IKubernetesObject + { + } + } + } + """.CreateCompilation(); + var expectedResult = """ + namespace Foo.Bar.Baz; + public partial class V1TestEntity + { + public V1TestEntity() + { + ApiVersion = "testing.dev/v1"; + Kind = "TestEntity"; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } + + [Fact] + public void Should_Generate_Default_Ctor_For_Global_Partial_Entity() + { + var inputCompilation = """ + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public partial class V1TestEntity : IKubernetesObject + { + } + """.CreateCompilation(); + var expectedResult = """ + public partial class V1TestEntity + { + public V1TestEntity() + { + ApiVersion = "testing.dev/v1"; + Kind = "TestEntity"; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } + + [Fact] + public void Should_Generate_Default_Ctor_For_Partial_Entity_With_Ctor() + { + var inputCompilation = """ + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + public partial class V1TestEntity : IKubernetesObject + { + public V1TestEntity(string name){} + } + """.CreateCompilation(); + var expectedResult = """ + public partial class V1TestEntity + { + public V1TestEntity() + { + ApiVersion = "testing.dev/v1"; + Kind = "TestEntity"; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } +} diff --git a/test/KubeOps.KubernetesClient.Test/IntegrationTestCollection.cs b/test/KubeOps.KubernetesClient.Test/IntegrationTestCollection.cs new file mode 100644 index 00000000..22bd05f3 --- /dev/null +++ b/test/KubeOps.KubernetesClient.Test/IntegrationTestCollection.cs @@ -0,0 +1,12 @@ +namespace KubeOps.KubernetesClient.Test; + +[CollectionDefinition(Name, DisableParallelization = true)] +public class IntegrationTestCollection +{ + public const string Name = "Integration Tests"; +} + +[Collection(IntegrationTestCollection.Name)] +public abstract class IntegrationTestBase +{ +} diff --git a/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs b/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs index bca621c9..56768474 100644 --- a/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs +++ b/test/KubeOps.KubernetesClient.Test/KubernetesClient.Test.cs @@ -4,7 +4,7 @@ namespace KubeOps.KubernetesClient.Test; -public class KubernetesClientTest : IDisposable +public class KubernetesClientTest : IntegrationTestBase, IDisposable { private readonly IKubernetesClient _client = new KubernetesClient(new("ConfigMap", "v1", null, "configmaps")); diff --git a/test/KubeOps.KubernetesClient.Test/KubernetesClientAsync.Test.cs b/test/KubeOps.KubernetesClient.Test/KubernetesClientAsync.Test.cs index 108dd3f3..c0c1b18c 100644 --- a/test/KubeOps.KubernetesClient.Test/KubernetesClientAsync.Test.cs +++ b/test/KubeOps.KubernetesClient.Test/KubernetesClientAsync.Test.cs @@ -4,7 +4,7 @@ namespace KubeOps.KubernetesClient.Test; -public class KubernetesClientAsyncTest : IDisposable +public class KubernetesClientAsyncTest : IntegrationTestBase, IDisposable { private readonly IKubernetesClient _client = new KubernetesClient(new("ConfigMap", "v1", null, "configmaps")); diff --git a/test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs b/test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs index 0b56d092..db4b59fe 100644 --- a/test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs +++ b/test/KubeOps.Operator.Test/Builder/OperatorBuilder.Test.cs @@ -1,78 +1,95 @@ -using FluentAssertions; - -using KubeOps.Abstractions.Builder; -using KubeOps.Abstractions.Controller; -using KubeOps.Abstractions.Entities; -using KubeOps.Abstractions.Finalizer; -using KubeOps.Abstractions.Queue; -using KubeOps.KubernetesClient; -using KubeOps.Operator.Builder; -using KubeOps.Operator.Finalizer; -using KubeOps.Operator.Queue; -using KubeOps.Operator.Test.TestEntities; -using KubeOps.Operator.Watcher; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace KubeOps.Operator.Test.Builder; - -public class OperatorBuilderTest -{ - private readonly IOperatorBuilder _builder = new OperatorBuilder(new ServiceCollection()); - - [Fact] - public void Should_Add_KubernetesClient_For_Entity() - { - _builder.AddEntity(new EntityMetadata("test", "v1", "testentities")); - - _builder.Services.Should().Contain(s => - s.ServiceType == typeof(IKubernetesClient) && - s.Lifetime == ServiceLifetime.Transient); - } - - [Fact] - public void Should_Add_ResourceWatcher_And_Controller() - { - _builder.AddController(); - - _builder.Services.Should().Contain(s => - 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.Lifetime == ServiceLifetime.Singleton); - _builder.Services.Should().Contain(s => - s.ServiceType == typeof(TimedEntityQueue) && - s.Lifetime == ServiceLifetime.Singleton); - _builder.Services.Should().Contain(s => - s.ServiceType == typeof(EntityRequeue) && - s.Lifetime == ServiceLifetime.Transient); - } - - [Fact] - public void Should_Add_Finalizer_Resources() - { - _builder.AddFinalizer(string.Empty); - - _builder.Services.Should().Contain(s => - s.ServiceType == typeof(TestFinalizer) && - s.Lifetime == ServiceLifetime.Transient); - _builder.Services.Should().Contain(s => - s.ServiceType == typeof(FinalizerRegistration) && - s.Lifetime == ServiceLifetime.Singleton); - _builder.Services.Should().Contain(s => - s.ServiceType == typeof(EntityFinalizerAttacher) && - s.Lifetime == ServiceLifetime.Transient); - } - - private class TestController : IEntityController - { - } - - private class TestFinalizer : IEntityFinalizer - { - } -} +using FluentAssertions; + +using k8s.Models; + +using KubeOps.Abstractions.Builder; +using KubeOps.Abstractions.Controller; +using KubeOps.Abstractions.Entities; +using KubeOps.Abstractions.Events; +using KubeOps.Abstractions.Finalizer; +using KubeOps.Abstractions.Queue; +using KubeOps.KubernetesClient; +using KubeOps.Operator.Builder; +using KubeOps.Operator.Finalizer; +using KubeOps.Operator.Queue; +using KubeOps.Operator.Test.TestEntities; +using KubeOps.Operator.Watcher; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace KubeOps.Operator.Test.Builder; + +public class OperatorBuilderTest +{ + private readonly IOperatorBuilder _builder = new OperatorBuilder(new ServiceCollection(), new()); + + [Fact] + public void Should_Add_Default_Resources() + { + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(OperatorSettings) && + s.Lifetime == ServiceLifetime.Singleton); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(IKubernetesClient) && + s.Lifetime == ServiceLifetime.Transient); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(EventPublisher) && + s.Lifetime == ServiceLifetime.Transient); + } + + [Fact] + public void Should_Add_Entity_Resources() + { + _builder.AddEntity(new EntityMetadata("test", "v1", "testentities")); + + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(IKubernetesClient) && + s.Lifetime == ServiceLifetime.Transient); + } + + [Fact] + public void Should_Add_Controller_Resources() + { + _builder.AddController(); + + _builder.Services.Should().Contain(s => + 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.Lifetime == ServiceLifetime.Singleton); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(TimedEntityQueue) && + s.Lifetime == ServiceLifetime.Singleton); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(EntityRequeue) && + s.Lifetime == ServiceLifetime.Transient); + } + + [Fact] + public void Should_Add_Finalizer_Resources() + { + _builder.AddFinalizer(string.Empty); + + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(TestFinalizer) && + s.Lifetime == ServiceLifetime.Transient); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(FinalizerRegistration) && + s.Lifetime == ServiceLifetime.Singleton); + _builder.Services.Should().Contain(s => + s.ServiceType == typeof(EntityFinalizerAttacher) && + s.Lifetime == ServiceLifetime.Transient); + } + + private class TestController : IEntityController + { + } + + private class TestFinalizer : IEntityFinalizer + { + } +} diff --git a/test/KubeOps.Operator.Test/Controller/CancelEntityRequeue.Integration.Test.cs b/test/KubeOps.Operator.Test/Controller/CancelEntityRequeue.Integration.Test.cs index 723559fc..022528f8 100644 --- a/test/KubeOps.Operator.Test/Controller/CancelEntityRequeue.Integration.Test.cs +++ b/test/KubeOps.Operator.Test/Controller/CancelEntityRequeue.Integration.Test.cs @@ -1,84 +1,81 @@ -using FluentAssertions; - -using k8s.Models; - -using KubeOps.Abstractions.Controller; -using KubeOps.Abstractions.Queue; -using KubeOps.KubernetesClient; -using KubeOps.Operator.Extensions; -using KubeOps.Operator.Queue; -using KubeOps.Operator.Test.TestEntities; -using KubeOps.Transpiler; - -using Microsoft.Extensions.DependencyInjection; - -namespace KubeOps.Operator.Test.Controller; - -public class CancelEntityRequeueIntegrationTest : IntegrationTestBase, IAsyncLifetime -{ - private static readonly InvocationCounter Mock = new(); - private IKubernetesClient _client = null!; - - public CancelEntityRequeueIntegrationTest(HostBuilder hostBuilder) : base(hostBuilder) - { - Mock.Clear(); - } - - [Fact] - public async Task Should_Cancel_Requeue_If_New_Event_Fires() - { - // This test fires the reconcile, which in turn requeues the entity. - // then immediately fires a new event, which should cancel the requeue. - - Mock.TargetInvocationCount = 2; - var e = await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); - e.Spec.Username = "changed"; - await _client.UpdateAsync(e); - await Mock.WaitForInvocations; - - Mock.Invocations.Count.Should().Be(2); - _hostBuilder.Services.GetRequiredService>().Count.Should().Be(0); - } - - public async Task InitializeAsync() - { - var meta = Entities.ToEntityMetadata(typeof(V1IntegrationTestEntity)).Metadata; - _client = new KubernetesClient(meta); - await _hostBuilder.ConfigureAndStart(builder => builder.Services - .AddSingleton(Mock) - .AddKubernetesOperator() - .AddControllerWithEntity(meta)); - } - - public async Task DisposeAsync() - { - var entities = await _client.ListAsync("default"); - await _client.DeleteAsync(entities); - _client.Dispose(); - } - - private class TestController : IEntityController - { - private readonly InvocationCounter _svc; - private readonly EntityRequeue _requeue; - - public TestController( - InvocationCounter svc, - EntityRequeue requeue) - { - _svc = svc; - _requeue = requeue; - } - - public Task ReconcileAsync(V1IntegrationTestEntity entity) - { - _svc.Invocation(entity); - if (_svc.Invocations.Count < 2) - { - _requeue(entity, TimeSpan.FromMilliseconds(1000)); - } - - return Task.CompletedTask; - } - } -} +using FluentAssertions; + +using KubeOps.Abstractions.Controller; +using KubeOps.Abstractions.Queue; +using KubeOps.KubernetesClient; +using KubeOps.Operator.Queue; +using KubeOps.Operator.Test.TestEntities; +using KubeOps.Transpiler; + +using Microsoft.Extensions.DependencyInjection; + +namespace KubeOps.Operator.Test.Controller; + +public class CancelEntityRequeueIntegrationTest : IntegrationTestBase, IAsyncLifetime +{ + private static readonly InvocationCounter Mock = new(); + private IKubernetesClient _client = null!; + + public CancelEntityRequeueIntegrationTest(HostBuilder hostBuilder) : base(hostBuilder) + { + Mock.Clear(); + } + + [Fact] + public async Task Should_Cancel_Requeue_If_New_Event_Fires() + { + // This test fires the reconcile, which in turn requeues the entity. + // then immediately fires a new event, which should cancel the requeue. + + Mock.TargetInvocationCount = 2; + var e = await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); + e.Spec.Username = "changed"; + await _client.UpdateAsync(e); + await Mock.WaitForInvocations; + + Mock.Invocations.Count.Should().Be(2); + _hostBuilder.Services.GetRequiredService>().Count.Should().Be(0); + } + + public async Task InitializeAsync() + { + var meta = Entities.ToEntityMetadata(typeof(V1IntegrationTestEntity)).Metadata; + _client = new KubernetesClient(meta); + await _hostBuilder.ConfigureAndStart(builder => builder.Services + .AddSingleton(Mock) + .AddKubernetesOperator() + .AddControllerWithEntity(meta)); + } + + public async Task DisposeAsync() + { + var entities = await _client.ListAsync("default"); + await _client.DeleteAsync(entities); + _client.Dispose(); + } + + private class TestController : IEntityController + { + private readonly InvocationCounter _svc; + private readonly EntityRequeue _requeue; + + public TestController( + InvocationCounter svc, + EntityRequeue requeue) + { + _svc = svc; + _requeue = requeue; + } + + public Task ReconcileAsync(V1IntegrationTestEntity entity) + { + _svc.Invocation(entity); + if (_svc.Invocations.Count < 2) + { + _requeue(entity, TimeSpan.FromMilliseconds(1000)); + } + + return Task.CompletedTask; + } + } +} diff --git a/test/KubeOps.Operator.Test/Controller/DeletedEntityRequeue.Integration.Test.cs b/test/KubeOps.Operator.Test/Controller/DeletedEntityRequeue.Integration.Test.cs index 5a31627c..800b7928 100644 --- a/test/KubeOps.Operator.Test/Controller/DeletedEntityRequeue.Integration.Test.cs +++ b/test/KubeOps.Operator.Test/Controller/DeletedEntityRequeue.Integration.Test.cs @@ -1,82 +1,79 @@ -using FluentAssertions; - -using k8s.Models; - -using KubeOps.Abstractions.Controller; -using KubeOps.Abstractions.Queue; -using KubeOps.KubernetesClient; -using KubeOps.Operator.Extensions; -using KubeOps.Operator.Queue; -using KubeOps.Operator.Test.TestEntities; -using KubeOps.Transpiler; - -using Microsoft.Extensions.DependencyInjection; - -namespace KubeOps.Operator.Test.Controller; - -public class DeletedEntityRequeueIntegrationTest : IntegrationTestBase, IAsyncLifetime -{ - private static readonly InvocationCounter Mock = new(); - private IKubernetesClient _client = null!; - - public DeletedEntityRequeueIntegrationTest(HostBuilder hostBuilder) : base(hostBuilder) - { - Mock.Clear(); - } - - [Fact] - public async Task Should_Cancel_Requeue_If_Entity_Is_Deleted() - { - Mock.TargetInvocationCount = 2; - var e = await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); - await _client.DeleteAsync(e); - await Mock.WaitForInvocations; - - Mock.Invocations.Count.Should().Be(2); - _hostBuilder.Services.GetRequiredService>().Count.Should().Be(0); - } - - public async Task InitializeAsync() - { - var meta = Entities.ToEntityMetadata(typeof(V1IntegrationTestEntity)).Metadata; - _client = new KubernetesClient(meta); - await _hostBuilder.ConfigureAndStart(builder => builder.Services - .AddSingleton(Mock) - .AddKubernetesOperator() - .AddControllerWithEntity(meta)); - } - - public async Task DisposeAsync() - { - var entities = await _client.ListAsync("default"); - await _client.DeleteAsync(entities); - _client.Dispose(); - } - - private class TestController : IEntityController - { - private readonly InvocationCounter _svc; - private readonly EntityRequeue _requeue; - - public TestController( - InvocationCounter svc, - EntityRequeue requeue) - { - _svc = svc; - _requeue = requeue; - } - - public Task ReconcileAsync(V1IntegrationTestEntity entity) - { - _svc.Invocation(entity); - _requeue(entity, TimeSpan.FromMilliseconds(1000)); - return Task.CompletedTask; - } - - public Task DeletedAsync(V1IntegrationTestEntity entity) - { - _svc.Invocation(entity); - return Task.CompletedTask; - } - } -} +using FluentAssertions; + +using KubeOps.Abstractions.Controller; +using KubeOps.Abstractions.Queue; +using KubeOps.KubernetesClient; +using KubeOps.Operator.Queue; +using KubeOps.Operator.Test.TestEntities; +using KubeOps.Transpiler; + +using Microsoft.Extensions.DependencyInjection; + +namespace KubeOps.Operator.Test.Controller; + +public class DeletedEntityRequeueIntegrationTest : IntegrationTestBase, IAsyncLifetime +{ + private static readonly InvocationCounter Mock = new(); + private IKubernetesClient _client = null!; + + public DeletedEntityRequeueIntegrationTest(HostBuilder hostBuilder) : base(hostBuilder) + { + Mock.Clear(); + } + + [Fact] + public async Task Should_Cancel_Requeue_If_Entity_Is_Deleted() + { + Mock.TargetInvocationCount = 2; + var e = await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); + await _client.DeleteAsync(e); + await Mock.WaitForInvocations; + + Mock.Invocations.Count.Should().Be(2); + _hostBuilder.Services.GetRequiredService>().Count.Should().Be(0); + } + + public async Task InitializeAsync() + { + var meta = Entities.ToEntityMetadata(typeof(V1IntegrationTestEntity)).Metadata; + _client = new KubernetesClient(meta); + await _hostBuilder.ConfigureAndStart(builder => builder.Services + .AddSingleton(Mock) + .AddKubernetesOperator() + .AddControllerWithEntity(meta)); + } + + public async Task DisposeAsync() + { + var entities = await _client.ListAsync("default"); + await _client.DeleteAsync(entities); + _client.Dispose(); + } + + private class TestController : IEntityController + { + private readonly InvocationCounter _svc; + private readonly EntityRequeue _requeue; + + public TestController( + InvocationCounter svc, + EntityRequeue requeue) + { + _svc = svc; + _requeue = requeue; + } + + public Task ReconcileAsync(V1IntegrationTestEntity entity) + { + _svc.Invocation(entity); + _requeue(entity, TimeSpan.FromMilliseconds(1000)); + return Task.CompletedTask; + } + + public Task DeletedAsync(V1IntegrationTestEntity entity) + { + _svc.Invocation(entity); + return Task.CompletedTask; + } + } +} diff --git a/test/KubeOps.Operator.Test/Controller/EntityController.Integration.Test.cs b/test/KubeOps.Operator.Test/Controller/EntityController.Integration.Test.cs index 8c354c9f..f81a280b 100644 --- a/test/KubeOps.Operator.Test/Controller/EntityController.Integration.Test.cs +++ b/test/KubeOps.Operator.Test/Controller/EntityController.Integration.Test.cs @@ -4,7 +4,6 @@ using KubeOps.Abstractions.Controller; using KubeOps.KubernetesClient; -using KubeOps.Operator.Extensions; using KubeOps.Operator.Test.TestEntities; using KubeOps.Transpiler; diff --git a/test/KubeOps.Operator.Test/Controller/EntityRequeue.Integration.Test.cs b/test/KubeOps.Operator.Test/Controller/EntityRequeue.Integration.Test.cs index a6fdab80..4a6304ef 100644 --- a/test/KubeOps.Operator.Test/Controller/EntityRequeue.Integration.Test.cs +++ b/test/KubeOps.Operator.Test/Controller/EntityRequeue.Integration.Test.cs @@ -1,86 +1,83 @@ -using FluentAssertions; - -using k8s.Models; - -using KubeOps.Abstractions.Controller; -using KubeOps.Abstractions.Queue; -using KubeOps.KubernetesClient; -using KubeOps.Operator.Extensions; -using KubeOps.Operator.Test.TestEntities; -using KubeOps.Transpiler; - -using Microsoft.Extensions.DependencyInjection; - -namespace KubeOps.Operator.Test.Controller; - -public class EntityRequeueIntegrationTest : IntegrationTestBase, IAsyncLifetime -{ - private static readonly InvocationCounter Mock = new(); - private IKubernetesClient _client = null!; - - public EntityRequeueIntegrationTest(HostBuilder hostBuilder) : base(hostBuilder) - { - Mock.Clear(); - } - - [Fact] - public async Task Should_Not_Queue_If_Not_Requested() - { - await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); - await Mock.WaitForInvocations; - - Mock.Invocations.Count.Should().Be(1); - } - - [Fact] - public async Task Should_Requeue_Entity_And_Reconcile() - { - Mock.TargetInvocationCount = 5; - await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); - await Mock.WaitForInvocations; - - Mock.Invocations.Count.Should().Be(5); - } - - public async Task InitializeAsync() - { - var meta = Entities.ToEntityMetadata(typeof(V1IntegrationTestEntity)).Metadata; - _client = new KubernetesClient(meta); - await _hostBuilder.ConfigureAndStart(builder => builder.Services - .AddSingleton(Mock) - .AddKubernetesOperator() - .AddControllerWithEntity(meta)); - } - - public async Task DisposeAsync() - { - var entities = await _client.ListAsync("default"); - await _client.DeleteAsync(entities); - _client.Dispose(); - } - - private class TestController : IEntityController - { - private readonly InvocationCounter _svc; - private readonly EntityRequeue _requeue; - - public TestController( - InvocationCounter svc, - EntityRequeue requeue) - { - _svc = svc; - _requeue = requeue; - } - - public Task ReconcileAsync(V1IntegrationTestEntity entity) - { - _svc.Invocation(entity); - if (_svc.Invocations.Count <= _svc.TargetInvocationCount) - { - _requeue(entity, TimeSpan.FromMilliseconds(1)); - } - - return Task.CompletedTask; - } - } -} +using FluentAssertions; + +using KubeOps.Abstractions.Controller; +using KubeOps.Abstractions.Queue; +using KubeOps.KubernetesClient; +using KubeOps.Operator.Test.TestEntities; +using KubeOps.Transpiler; + +using Microsoft.Extensions.DependencyInjection; + +namespace KubeOps.Operator.Test.Controller; + +public class EntityRequeueIntegrationTest : IntegrationTestBase, IAsyncLifetime +{ + private static readonly InvocationCounter Mock = new(); + private IKubernetesClient _client = null!; + + public EntityRequeueIntegrationTest(HostBuilder hostBuilder) : base(hostBuilder) + { + Mock.Clear(); + } + + [Fact] + public async Task Should_Not_Queue_If_Not_Requested() + { + await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); + await Mock.WaitForInvocations; + + Mock.Invocations.Count.Should().Be(1); + } + + [Fact] + public async Task Should_Requeue_Entity_And_Reconcile() + { + Mock.TargetInvocationCount = 5; + await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); + await Mock.WaitForInvocations; + + Mock.Invocations.Count.Should().Be(5); + } + + public async Task InitializeAsync() + { + var meta = Entities.ToEntityMetadata(typeof(V1IntegrationTestEntity)).Metadata; + _client = new KubernetesClient(meta); + await _hostBuilder.ConfigureAndStart(builder => builder.Services + .AddSingleton(Mock) + .AddKubernetesOperator() + .AddControllerWithEntity(meta)); + } + + public async Task DisposeAsync() + { + var entities = await _client.ListAsync("default"); + await _client.DeleteAsync(entities); + _client.Dispose(); + } + + private class TestController : IEntityController + { + private readonly InvocationCounter _svc; + private readonly EntityRequeue _requeue; + + public TestController( + InvocationCounter svc, + EntityRequeue requeue) + { + _svc = svc; + _requeue = requeue; + } + + public Task ReconcileAsync(V1IntegrationTestEntity entity) + { + _svc.Invocation(entity); + if (_svc.Invocations.Count <= _svc.TargetInvocationCount) + { + _requeue(entity, TimeSpan.FromMilliseconds(1)); + } + + return Task.CompletedTask; + } + } +} diff --git a/test/KubeOps.Operator.Test/Events/EventPublisher.Integration.Test.cs b/test/KubeOps.Operator.Test/Events/EventPublisher.Integration.Test.cs new file mode 100644 index 00000000..949d0cba --- /dev/null +++ b/test/KubeOps.Operator.Test/Events/EventPublisher.Integration.Test.cs @@ -0,0 +1,111 @@ +using System.Security.Cryptography; +using System.Text; + +using FluentAssertions; + +using k8s.Models; + +using KubeOps.Abstractions.Controller; +using KubeOps.Abstractions.Events; +using KubeOps.Abstractions.Queue; +using KubeOps.KubernetesClient; +using KubeOps.Operator.Test.TestEntities; +using KubeOps.Transpiler; + +using Microsoft.Extensions.DependencyInjection; + +namespace KubeOps.Operator.Test.Events; + +public class EventPublisherIntegrationTest : IntegrationTestBase, IAsyncLifetime +{ + private static readonly InvocationCounter Mock = new(); + private IKubernetesClient _client = null!; + + public EventPublisherIntegrationTest(HostBuilder hostBuilder) : base(hostBuilder) + { + Mock.Clear(); + } + + [Fact] + public async Task Should_Create_New_Event() + { + const string eventName = "test-entity.default.REASON.message.Normal"; + var encodedEventName = + Convert.ToHexString( + SHA512.HashData( + Encoding.UTF8.GetBytes(eventName))); + + await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); + await Mock.WaitForInvocations; + + var eventClient = _hostBuilder.Services.GetRequiredService>(); + var e = await eventClient.GetAsync(encodedEventName, "default"); + e!.Count.Should().Be(1); + e.Metadata.Annotations.Should().Contain(a => a.Key == "originalName" && a.Value == eventName); + } + + [Fact] + public async Task Should_Increase_Count_On_Existing_Event() + { + Mock.TargetInvocationCount = 5; + const string eventName = "test-entity.default.REASON.message.Normal"; + var encodedEventName = + Convert.ToHexString( + SHA512.HashData( + Encoding.UTF8.GetBytes(eventName))); + + await _client.CreateAsync(new V1IntegrationTestEntity("test-entity", "username", "default")); + await Mock.WaitForInvocations; + + var eventClient = _hostBuilder.Services.GetRequiredService>(); + var e = await eventClient.GetAsync(encodedEventName, "default"); + e!.Count.Should().Be(5); + e.Metadata.Annotations.Should().Contain(a => a.Key == "originalName" && a.Value == eventName); + } + + public async Task InitializeAsync() + { + var meta = Entities.ToEntityMetadata(typeof(V1IntegrationTestEntity)).Metadata; + _client = new KubernetesClient(meta); + await _hostBuilder.ConfigureAndStart(builder => builder.Services + .AddSingleton(Mock) + .AddKubernetesOperator() + .AddControllerWithEntity(meta)); + } + + public async Task DisposeAsync() + { + await _client.DeleteAsync(await _client.ListAsync("default")); + using var eventClient = _hostBuilder.Services.GetRequiredService>(); + await eventClient.DeleteAsync(await eventClient.ListAsync("default")); + _client.Dispose(); + } + + private class TestController : IEntityController + { + private readonly InvocationCounter _svc; + private readonly EntityRequeue _requeue; + private readonly EventPublisher _eventPublisher; + + public TestController( + InvocationCounter svc, + EntityRequeue requeue, + EventPublisher eventPublisher) + { + _svc = svc; + _requeue = requeue; + _eventPublisher = eventPublisher; + } + + public async Task ReconcileAsync(V1IntegrationTestEntity entity) + { + await _eventPublisher(entity, "REASON", "message"); + _svc.Invocation(entity); + + if (_svc.Invocations.Count < _svc.TargetInvocationCount) + { + _requeue(entity, TimeSpan.FromMilliseconds(1)); + } + } + } +} diff --git a/test/KubeOps.Operator.Test/Finalizer/EntityFinalizer.Integration.Test.cs b/test/KubeOps.Operator.Test/Finalizer/EntityFinalizer.Integration.Test.cs index c3988517..ee1c516e 100644 --- a/test/KubeOps.Operator.Test/Finalizer/EntityFinalizer.Integration.Test.cs +++ b/test/KubeOps.Operator.Test/Finalizer/EntityFinalizer.Integration.Test.cs @@ -5,7 +5,6 @@ using KubeOps.Abstractions.Controller; using KubeOps.Abstractions.Finalizer; using KubeOps.KubernetesClient; -using KubeOps.Operator.Extensions; using KubeOps.Operator.Test.TestEntities; using KubeOps.Transpiler;