From c0e5431ff6109265d3dd3ad60dc7477158d452b4 Mon Sep 17 00:00:00 2001 From: Carlos Landeras Date: Fri, 11 Sep 2020 00:20:16 +0200 Subject: [PATCH 1/4] [K8s Operator] Cluster and Namespaced support --- build/dependencies.props | 2 +- deploy/operator/crd/healthcheck-crd.yaml | 6 ++ src/HealthChecks.UI.K8s.Operator/Constants.cs | 6 ++ .../Crd/HealthCheckResourceSpec.cs | 1 + .../Diagnostics/OperatorDiagnostics.cs | 2 +- .../Handlers/IKubernetesClient.cs | 6 ++ .../Handlers/NotificationHandler.cs | 44 ++++++++++++ .../Operator/ClusterServiceWatcher.cs | 68 +++++++++++++++++++ .../Operator/HealthChecksOperator.cs | 44 ++++++++++-- .../Operator/KubernetesAddressFactory.cs | 7 +- ...Watcher.cs => NamespacedServiceWatcher.cs} | 19 +++--- src/HealthChecks.UI.K8s.Operator/Program.cs | 5 +- 12 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 src/HealthChecks.UI.K8s.Operator/Handlers/IKubernetesClient.cs create mode 100644 src/HealthChecks.UI.K8s.Operator/Handlers/NotificationHandler.cs create mode 100644 src/HealthChecks.UI.K8s.Operator/Operator/ClusterServiceWatcher.cs rename src/HealthChecks.UI.K8s.Operator/Operator/{HealthCheckServiceWatcher.cs => NamespacedServiceWatcher.cs} (86%) diff --git a/build/dependencies.props b/build/dependencies.props index 90161cd01a..e6e0d9323f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -78,7 +78,7 @@ 4.1.0 3.3.0 1.18.1 - 1.6.10 + 2.0.29 1.0.19 9.1.4 3.1.2 diff --git a/deploy/operator/crd/healthcheck-crd.yaml b/deploy/operator/crd/healthcheck-crd.yaml index 5c4abea9d8..30690c7fc4 100644 --- a/deploy/operator/crd/healthcheck-crd.yaml +++ b/deploy/operator/crd/healthcheck-crd.yaml @@ -23,6 +23,11 @@ spec: properties: name: type: string + scope: + type: string + enum: + - Cluster + - Namespaced serviceType: type: string enum: @@ -99,4 +104,5 @@ spec: required: - name + - scope - servicesLabel diff --git a/src/HealthChecks.UI.K8s.Operator/Constants.cs b/src/HealthChecks.UI.K8s.Operator/Constants.cs index 79e51cf602..b5872324d4 100644 --- a/src/HealthChecks.UI.K8s.Operator/Constants.cs +++ b/src/HealthChecks.UI.K8s.Operator/Constants.cs @@ -29,6 +29,12 @@ internal class Operation public const string Delete = "Delete"; public const string Patch = "Patch"; } + + internal class Scope + { + public const string Cluster = "Cluster"; + public const string Namespaced = "Namespaced"; + } } } } diff --git a/src/HealthChecks.UI.K8s.Operator/Crd/HealthCheckResourceSpec.cs b/src/HealthChecks.UI.K8s.Operator/Crd/HealthCheckResourceSpec.cs index 7ab1304bc8..a70964a2a5 100644 --- a/src/HealthChecks.UI.K8s.Operator/Crd/HealthCheckResourceSpec.cs +++ b/src/HealthChecks.UI.K8s.Operator/Crd/HealthCheckResourceSpec.cs @@ -6,6 +6,7 @@ namespace HealthChecks.UI.K8s.Operator public class HealthCheckResourceSpec { public string Name { get; set; } + public string Scope { get; set; } public string PortNumber { get; set; } public string ServiceType { get; set; } public string UiPath { get; set; } diff --git a/src/HealthChecks.UI.K8s.Operator/Diagnostics/OperatorDiagnostics.cs b/src/HealthChecks.UI.K8s.Operator/Diagnostics/OperatorDiagnostics.cs index 1ec10109d7..4dbce284fe 100644 --- a/src/HealthChecks.UI.K8s.Operator/Diagnostics/OperatorDiagnostics.cs +++ b/src/HealthChecks.UI.K8s.Operator/Diagnostics/OperatorDiagnostics.cs @@ -39,7 +39,7 @@ public void OperatorThrow(Exception exception) public void ServiceWatcherThrow(Exception exception) { - _logger.LogError(exception, "The operator service watcher threw an unhandled exception"); + _logger.LogError(exception.Message, "The operator service watcher threw an unhandled exception"); } public void UiPathConfigured(string path, string value) diff --git a/src/HealthChecks.UI.K8s.Operator/Handlers/IKubernetesClient.cs b/src/HealthChecks.UI.K8s.Operator/Handlers/IKubernetesClient.cs new file mode 100644 index 0000000000..84f8c5691e --- /dev/null +++ b/src/HealthChecks.UI.K8s.Operator/Handlers/IKubernetesClient.cs @@ -0,0 +1,6 @@ +namespace HealthChecks.UI.K8s.Operator.Handlers +{ + public interface IKubernetesClient + { + } +} \ No newline at end of file diff --git a/src/HealthChecks.UI.K8s.Operator/Handlers/NotificationHandler.cs b/src/HealthChecks.UI.K8s.Operator/Handlers/NotificationHandler.cs new file mode 100644 index 0000000000..02cedfc542 --- /dev/null +++ b/src/HealthChecks.UI.K8s.Operator/Handlers/NotificationHandler.cs @@ -0,0 +1,44 @@ +using k8s; +using k8s.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace HealthChecks.UI.K8s.Operator.Handlers +{ + public class NotificationHandler + { + private readonly IKubernetes _client; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public NotificationHandler(IKubernetes client, IHttpClientFactory httpClientFactory, ILogger logger) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + public async Task NotifyDiscoveredServiceAsync(WatchEventType type, V1Service service, HealthCheckResource resource) + { + var uiService = await _client.ListNamespacedOwnedServiceAsync(resource.Metadata.NamespaceProperty, resource.Metadata.Uid); + var secret = await _client.ListNamespacedOwnedSecretAsync(resource.Metadata.NamespaceProperty, resource.Metadata.Uid); + + if (!service.Metadata.Labels.ContainsKey(resource.Spec.ServicesLabel)) + { + type = WatchEventType.Deleted; + } + + await HealthChecksPushService.PushNotification( + type, + resource, + uiService, + service, + secret, + _logger, + _httpClientFactory); + } + } +} diff --git a/src/HealthChecks.UI.K8s.Operator/Operator/ClusterServiceWatcher.cs b/src/HealthChecks.UI.K8s.Operator/Operator/ClusterServiceWatcher.cs new file mode 100644 index 0000000000..50561d6717 --- /dev/null +++ b/src/HealthChecks.UI.K8s.Operator/Operator/ClusterServiceWatcher.cs @@ -0,0 +1,68 @@ +using HealthChecks.UI.K8s.Operator.Diagnostics; +using HealthChecks.UI.K8s.Operator.Handlers; +using k8s; +using k8s.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace HealthChecks.UI.K8s.Operator.Operator +{ + internal class ClusterServiceWatcher + { + private readonly IKubernetes _client; + private readonly ILogger _logger; + private readonly OperatorDiagnostics _diagnostics; + private readonly NotificationHandler _notificationHandler; + private readonly IHttpClientFactory _httpClientFactory; + private Watcher _watcher; + public ClusterServiceWatcher( + IKubernetes client, + ILogger logger, + OperatorDiagnostics diagnostics, + NotificationHandler notificationHandler, + IHttpClientFactory httpClientFactory) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); + _notificationHandler = notificationHandler; + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + _notificationHandler = notificationHandler ?? throw new ArgumentNullException(nameof(notificationHandler)); + } + + internal Task Watch(HealthCheckResource resource, CancellationToken token) + { + var response = _client.ListServiceForAllNamespacesWithHttpMessagesAsync( + labelSelector: $"{resource.Spec.ServicesLabel}", + watch: true, + cancellationToken: token); + + _watcher = response.Watch( + onEvent: async (type, item) => await _notificationHandler.NotifyDiscoveredServiceAsync(type, item, resource), + onError: e => _diagnostics.ServiceWatcherThrow(e) + ); + + _diagnostics.ServiceWatcherStarting("All"); + + return Task.CompletedTask; + } + + internal void Stopwatch(HealthCheckResource resource) + { + Dispose(); + } + + public void Dispose() + { + if(_watcher != null && _watcher.Watching) + { + _watcher.Dispose(); + } + } + } +} diff --git a/src/HealthChecks.UI.K8s.Operator/Operator/HealthChecksOperator.cs b/src/HealthChecks.UI.K8s.Operator/Operator/HealthChecksOperator.cs index 254a573e83..f207b84d92 100644 --- a/src/HealthChecks.UI.K8s.Operator/Operator/HealthChecksOperator.cs +++ b/src/HealthChecks.UI.K8s.Operator/Operator/HealthChecksOperator.cs @@ -1,5 +1,6 @@ using HealthChecks.UI.K8s.Operator.Controller; using HealthChecks.UI.K8s.Operator.Diagnostics; +using HealthChecks.UI.K8s.Operator.Operator; using k8s; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -7,6 +8,7 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using static HealthChecks.UI.K8s.Operator.Constants; namespace HealthChecks.UI.K8s.Operator { @@ -15,7 +17,8 @@ internal class HealthChecksOperator : IHostedService private Watcher _watcher; private readonly IKubernetes _client; private readonly IHealthChecksController _controller; - private readonly HealthCheckServiceWatcher _serviceWatcher; + private readonly NamespacedServiceWatcher _serviceWatcher; + private readonly ClusterServiceWatcher _clusterServiceWatcher; private readonly OperatorDiagnostics _diagnostics; private readonly ILogger _logger; private readonly CancellationTokenSource _operatorCts = new CancellationTokenSource(); @@ -26,7 +29,8 @@ internal class HealthChecksOperator : IHostedService public HealthChecksOperator( IKubernetes client, IHealthChecksController controller, - HealthCheckServiceWatcher serviceWatcher, + NamespacedServiceWatcher serviceWatcher, + ClusterServiceWatcher clusterServiceWatcher, OperatorDiagnostics diagnostics, ILogger logger) { @@ -34,6 +38,7 @@ public HealthChecksOperator( _client = client ?? throw new ArgumentNullException(nameof(client)); _controller = controller ?? throw new ArgumentNullException(nameof(controller)); _serviceWatcher = serviceWatcher ?? throw new ArgumentNullException(nameof(serviceWatcher)); + _clusterServiceWatcher = clusterServiceWatcher ?? throw new ArgumentNullException(nameof(clusterServiceWatcher)); _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -63,6 +68,7 @@ public Task StopAsync(CancellationToken cancellationToken) } _serviceWatcher.Dispose(); + _clusterServiceWatcher.Dispose(); _channel.Writer.Complete(); return Task.CompletedTask; @@ -111,13 +117,13 @@ private async Task OperatorListener() { await _controller.DeployAsync(item.Resource); await WaitForAvailableReplicas(item.Resource); - await _serviceWatcher.Watch(item.Resource, _operatorCts.Token); + await StartServiceWatcher(item.Resource); }); } else if (item.EventType == WatchEventType.Deleted) { await _controller.DeleteDeploymentAsync(item.Resource); - _serviceWatcher.Stopwatch(item.Resource); + StopServiceWatcher(item.Resource); } } catch (Exception ex) @@ -128,6 +134,36 @@ private async Task OperatorListener() } } + private async Task StartServiceWatcher(HealthCheckResource resource) + { + Func startWatcher = async () => await _serviceWatcher.Watch(resource, _operatorCts.Token); + Func startClusterWatcher = async () => await _clusterServiceWatcher.Watch(resource, _operatorCts.Token); + + var start = resource.Spec.Scope switch + { + Deployment.Scope.Namespaced => startWatcher, + Deployment.Scope.Cluster => startClusterWatcher, + _ => throw new ArgumentOutOfRangeException(nameof(resource.Spec.Scope)) + }; + + await start(); + } + + private void StopServiceWatcher(HealthCheckResource resource) + { + Action stopWatcher = () => _serviceWatcher.Stopwatch(resource); + Action stopClusterWatcher = () => _clusterServiceWatcher.Stopwatch(resource); + + var stop = resource.Spec.Scope switch + { + Deployment.Scope.Namespaced => stopWatcher, + Deployment.Scope.Cluster => stopClusterWatcher, + _ => throw new ArgumentOutOfRangeException(nameof(resource.Spec.Scope)) + }; + + stop(); + } + private async Task WaitForAvailableReplicas(HealthCheckResource resource) { int retries = 1; diff --git a/src/HealthChecks.UI.K8s.Operator/Operator/KubernetesAddressFactory.cs b/src/HealthChecks.UI.K8s.Operator/Operator/KubernetesAddressFactory.cs index da8a133f3e..be56da49e4 100644 --- a/src/HealthChecks.UI.K8s.Operator/Operator/KubernetesAddressFactory.cs +++ b/src/HealthChecks.UI.K8s.Operator/Operator/KubernetesAddressFactory.cs @@ -10,7 +10,12 @@ public static string CreateAddress(V1Service service, HealthCheckResource resour { var defaultPort = int.Parse(resource.Spec.PortNumber ?? Constants.DefaultPort); var port = GetServicePort(service)?.Port ?? defaultPort; - var address = service.Spec.ClusterIP; + var address = GetLoadBalancerAddress(service); + + if(string.IsNullOrEmpty(address)) + { + address = service.Spec.ClusterIP; + } string healthScheme = resource.Spec.HealthChecksScheme; diff --git a/src/HealthChecks.UI.K8s.Operator/Operator/HealthCheckServiceWatcher.cs b/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs similarity index 86% rename from src/HealthChecks.UI.K8s.Operator/Operator/HealthCheckServiceWatcher.cs rename to src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs index 113ae49508..75b754436e 100644 --- a/src/HealthChecks.UI.K8s.Operator/Operator/HealthCheckServiceWatcher.cs +++ b/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs @@ -1,4 +1,5 @@ using HealthChecks.UI.K8s.Operator.Diagnostics; +using HealthChecks.UI.K8s.Operator.Handlers; using k8s; using k8s.Models; using Microsoft.Extensions.Logging; @@ -11,24 +12,28 @@ namespace HealthChecks.UI.K8s.Operator { - internal class HealthCheckServiceWatcher : IDisposable + internal class NamespacedServiceWatcher : IDisposable { private readonly IKubernetes _client; private readonly ILogger _logger; private readonly OperatorDiagnostics _diagnostics; + private readonly NotificationHandler _notificationHandler; private readonly IHttpClientFactory _httpClientFactory; private Dictionary> _watchers = new Dictionary>(); - public HealthCheckServiceWatcher( + public NamespacedServiceWatcher( IKubernetes client, ILogger logger, OperatorDiagnostics diagnostics, + NotificationHandler notificationHandler, IHttpClientFactory httpClientFactory) { _client = client ?? throw new ArgumentNullException(nameof(client)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); + _notificationHandler = notificationHandler; _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + _notificationHandler = notificationHandler ?? throw new ArgumentNullException(nameof(notificationHandler)); } internal Task Watch(HealthCheckResource resource, CancellationToken token) @@ -36,7 +41,7 @@ internal Task Watch(HealthCheckResource resource, CancellationToken token) Func filter = (k) => k.Metadata.NamespaceProperty == resource.Metadata.NamespaceProperty; if (!_watchers.Keys.Any(filter)) - { + { var response = _client.ListNamespacedServiceWithHttpMessagesAsync( namespaceParameter: resource.Metadata.NamespaceProperty, labelSelector: $"{resource.Spec.ServicesLabel}", @@ -44,7 +49,7 @@ internal Task Watch(HealthCheckResource resource, CancellationToken token) cancellationToken: token); var watcher = response.Watch( - onEvent: async (type, item) => await OnServiceDiscoveredAsync(type, item, resource), + onEvent: async (type, item) => await _notificationHandler.NotifyDiscoveredServiceAsync(type, item, resource), onError: e => _diagnostics.ServiceWatcherThrow(e) ); @@ -98,11 +103,5 @@ public void Dispose() if (w != null && w.Watching) w.Dispose(); }); } - - private class ServiceWatch - { - WatchEventType EventType { get; set; } - V1Service Service { get; set; } - } } } \ No newline at end of file diff --git a/src/HealthChecks.UI.K8s.Operator/Program.cs b/src/HealthChecks.UI.K8s.Operator/Program.cs index 2dd8569bb0..2cf656f0a4 100644 --- a/src/HealthChecks.UI.K8s.Operator/Program.cs +++ b/src/HealthChecks.UI.K8s.Operator/Program.cs @@ -8,6 +8,7 @@ using Serilog; using Serilog.Events; using HealthChecks.UI.K8s.Operator.Diagnostics; +using HealthChecks.UI.K8s.Operator.Operator; namespace HealthChecks.UI.K8s.Operator { @@ -49,7 +50,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); }).ConfigureLogging((context, builder) => { From f1059a9e7505ee40ab8005fe2c644200bb4446b8 Mon Sep 17 00:00:00 2001 From: Carlos Landeras Date: Fri, 11 Sep 2020 00:21:16 +0200 Subject: [PATCH 2/4] clean: Remove empty class --- .../Handlers/IKubernetesClient.cs | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/HealthChecks.UI.K8s.Operator/Handlers/IKubernetesClient.cs diff --git a/src/HealthChecks.UI.K8s.Operator/Handlers/IKubernetesClient.cs b/src/HealthChecks.UI.K8s.Operator/Handlers/IKubernetesClient.cs deleted file mode 100644 index 84f8c5691e..0000000000 --- a/src/HealthChecks.UI.K8s.Operator/Handlers/IKubernetesClient.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace HealthChecks.UI.K8s.Operator.Handlers -{ - public interface IKubernetesClient - { - } -} \ No newline at end of file From 6ad2d065cacf5139624e97686969e7d19d7cb1de Mon Sep 17 00:00:00 2001 From: Carlos Landeras Date: Fri, 11 Sep 2020 00:35:59 +0200 Subject: [PATCH 3/4] [K8s Operator] Add Scope documentation --- doc/k8s-operator.md | 15 +++++++++++++++ .../Operator/NamespacedServiceWatcher.cs | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/k8s-operator.md b/doc/k8s-operator.md index 00d6e46546..22e0cda6e6 100644 --- a/doc/k8s-operator.md +++ b/doc/k8s-operator.md @@ -38,8 +38,21 @@ The [HealthCheck operator definition](https://github.com/Xabaril/AspNetCore.Diag | Field | Description | | ------------- | :--------------------------------------------------------------------------------- | | name | Name of the healthcheck resource | +| scope | Cluster / Namespaced | | servicesLabel | The label the operator service watcher will use to detected healthchecks endpoints | + +### Scope definition (Cluster or Namespaced) + +The scope field (Cluster or Namespaced) is mandatory and will specify to the operator whether it should watch for healthchecks services in the +same namespace where the UI resource is created or watch to all services in all namespaces. + +If you wan't to have different UI's for different namespaced services you should use **Namespaced** + +If you wan't to have a single UI that monitors all the healthchecks from the cluster you should use **Cluster** + +Note: The UI resources created by the operator (deployment, service, configmap, secret, etc) will always be created in the metadata specified namespace. + ### Optional fields | Field | Description | Default | @@ -76,6 +89,8 @@ metadata: namespace: demo spec: name: healthchecks-ui + scope: Namespaced #The UI will be created at specified namespace (demo) and will watch healthchecks services in demo namespace only + #scope: Cluster The UI will be created at specified namespace (demo) but will watch healthcheck services across all namespaces servicesLabel: HealthChecks serviceType: LoadBalancer stylesheetContent: > diff --git a/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs b/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs index 75b754436e..df8c3e3e87 100644 --- a/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs +++ b/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs @@ -41,7 +41,7 @@ internal Task Watch(HealthCheckResource resource, CancellationToken token) Func filter = (k) => k.Metadata.NamespaceProperty == resource.Metadata.NamespaceProperty; if (!_watchers.Keys.Any(filter)) - { + { var response = _client.ListNamespacedServiceWithHttpMessagesAsync( namespaceParameter: resource.Metadata.NamespaceProperty, labelSelector: $"{resource.Spec.ServicesLabel}", From 0e35c7ca909241588a31b93665a15114a1a42731 Mon Sep 17 00:00:00 2001 From: Carlos Landeras Date: Fri, 11 Sep 2020 00:49:01 +0200 Subject: [PATCH 4/4] clean: operator code cleanup --- .../Operator/ClusterServiceWatcher.cs | 3 --- .../Operator/NamespacedServiceWatcher.cs | 1 - 2 files changed, 4 deletions(-) diff --git a/src/HealthChecks.UI.K8s.Operator/Operator/ClusterServiceWatcher.cs b/src/HealthChecks.UI.K8s.Operator/Operator/ClusterServiceWatcher.cs index 50561d6717..cbe16d1aab 100644 --- a/src/HealthChecks.UI.K8s.Operator/Operator/ClusterServiceWatcher.cs +++ b/src/HealthChecks.UI.K8s.Operator/Operator/ClusterServiceWatcher.cs @@ -18,7 +18,6 @@ internal class ClusterServiceWatcher private readonly ILogger _logger; private readonly OperatorDiagnostics _diagnostics; private readonly NotificationHandler _notificationHandler; - private readonly IHttpClientFactory _httpClientFactory; private Watcher _watcher; public ClusterServiceWatcher( IKubernetes client, @@ -30,8 +29,6 @@ public ClusterServiceWatcher( _client = client ?? throw new ArgumentNullException(nameof(client)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); - _notificationHandler = notificationHandler; - _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); _notificationHandler = notificationHandler ?? throw new ArgumentNullException(nameof(notificationHandler)); } diff --git a/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs b/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs index df8c3e3e87..bf1d23ca4a 100644 --- a/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs +++ b/src/HealthChecks.UI.K8s.Operator/Operator/NamespacedServiceWatcher.cs @@ -31,7 +31,6 @@ public NamespacedServiceWatcher( _client = client ?? throw new ArgumentNullException(nameof(client)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); - _notificationHandler = notificationHandler; _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); _notificationHandler = notificationHandler ?? throw new ArgumentNullException(nameof(notificationHandler)); }