diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 8d43eee..09a252a 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -11,7 +11,7 @@ on: - ".github/workflows/**" env: - version: 7.1.${{github.run_number}} + version: 9.0.${{github.run_number}} imageRepository: "emberstack/kubernetes-reflector" DOCKER_CLI_EXPERIMENTAL: "enabled" diff --git a/README.md b/README.md index 84761bb..a09cbe3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ If you need help or found a bug, please feel free to open an Issue on GitHub (ht Reflector can be deployed either manually or using Helm (recommended). ### Prerequisites -- Kubernetes 1.14+ +- Kubernetes 1.22+ - Helm 3 (if deployed using Helm) #### Deployment using Helm diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..2bbe6ca --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,34 @@ + + + + false + true + enable + enable + + embedded + true + + $(MSBuildThisFileDirectory)..\ + + true + + + CS1591;CS1571;CS1573;CS1574;CS1723;NU1901;NU1902;NU1903; + + + + + + + + + + + + + false + trx%3bLogFileName=$(MSBuildProjectName).trx + $(MSBuildThisFileDirectory).artifacts/TestResults + + \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props new file mode 100644 index 0000000..fe326b7 --- /dev/null +++ b/src/Directory.Packages.props @@ -0,0 +1,16 @@ + + + true + false + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector.sln b/src/ES.Kubernetes.Reflector.sln index f726f1a..49b0238 100644 --- a/src/ES.Kubernetes.Reflector.sln +++ b/src/ES.Kubernetes.Reflector.sln @@ -5,6 +5,11 @@ VisualStudioVersion = 17.0.31710.8 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ES.Kubernetes.Reflector", "ES.Kubernetes.Reflector\ES.Kubernetes.Reflector.csproj", "{96CDE0CF-7782-490B-8AF6-4219DB0236B3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "....Solution Items", "....Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/ES.Kubernetes.Reflector/Core/ConfigMapMirror.cs b/src/ES.Kubernetes.Reflector/Core/ConfigMapMirror.cs index 430631e..8a37efd 100644 --- a/src/ES.Kubernetes.Reflector/Core/ConfigMapMirror.cs +++ b/src/ES.Kubernetes.Reflector/Core/ConfigMapMirror.cs @@ -6,21 +6,23 @@ namespace ES.Kubernetes.Reflector.Core; -public class ConfigMapMirror : ResourceMirror +public class ConfigMapMirror(ILogger logger, IServiceProvider serviceProvider) + : ResourceMirror(logger, serviceProvider) { - public ConfigMapMirror(ILogger logger, IKubernetes client) : base(logger, client) - { - } + private readonly IServiceProvider _serviceProvider = serviceProvider; protected override async Task OnResourceWithNameList(string itemRefName) { - return (await Client.CoreV1.ListConfigMapForAllNamespacesAsync(fieldSelector: $"metadata.name={itemRefName}")).Items + using var client = _serviceProvider.GetRequiredService(); + return (await client.CoreV1.ListConfigMapForAllNamespacesAsync(fieldSelector: $"metadata.name={itemRefName}")) + .Items .ToArray(); } - protected override Task OnResourceApplyPatch(V1Patch patch, KubeRef refId) + protected override async Task OnResourceApplyPatch(V1Patch patch, KubeRef refId) { - return Client.CoreV1.PatchNamespacedConfigMapAsync(patch, refId.Name, refId.Namespace); + using var client = _serviceProvider.GetRequiredService(); + await client.CoreV1.PatchNamespacedConfigMapAsync(patch, refId.Name, refId.Namespace); } protected override Task OnResourceConfigurePatch(V1ConfigMap source, JsonPatchDocument patchDoc) @@ -30,9 +32,10 @@ protected override Task OnResourceConfigurePatch(V1ConfigMap source, JsonPatchDo return Task.CompletedTask; } - protected override Task OnResourceCreate(V1ConfigMap item, string ns) + protected override async Task OnResourceCreate(V1ConfigMap item, string ns) { - return Client.CoreV1.CreateNamespacedConfigMapAsync(item, ns); + using var client = _serviceProvider.GetRequiredService(); + await client.CoreV1.CreateNamespacedConfigMapAsync(item, ns); } protected override Task OnResourceClone(V1ConfigMap sourceResource) @@ -46,13 +49,15 @@ protected override Task OnResourceClone(V1ConfigMap sourceResource) }); } - protected override Task OnResourceDelete(KubeRef resourceId) + protected override async Task OnResourceDelete(KubeRef resourceId) { - return Client.CoreV1.DeleteNamespacedConfigMapAsync(resourceId.Name, resourceId.Namespace); + using var client = _serviceProvider.GetRequiredService(); + await client.CoreV1.DeleteNamespacedConfigMapAsync(resourceId.Name, resourceId.Namespace); } - protected override Task OnResourceGet(KubeRef refId) + protected override async Task OnResourceGet(KubeRef refId) { - return Client.CoreV1.ReadNamespacedConfigMapAsync(refId.Name, refId.Namespace); + using var client = _serviceProvider.GetRequiredService(); + return await client.CoreV1.ReadNamespacedConfigMapAsync(refId.Name, refId.Namespace); } } \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/Core/ConfigMapWatcher.cs b/src/ES.Kubernetes.Reflector/Core/ConfigMapWatcher.cs index 8bda2d1..a53881b 100644 --- a/src/ES.Kubernetes.Reflector/Core/ConfigMapWatcher.cs +++ b/src/ES.Kubernetes.Reflector/Core/ConfigMapWatcher.cs @@ -8,18 +8,18 @@ namespace ES.Kubernetes.Reflector.Core; -public class ConfigMapWatcher : WatcherBackgroundService +public class ConfigMapWatcher( + ILogger logger, + IMediator mediator, + IServiceProvider serviceProvider, + IOptionsMonitor options) + : WatcherBackgroundService(logger, mediator, serviceProvider, options) { - public ConfigMapWatcher(ILogger logger, IMediator mediator, IKubernetes client, - IOptionsMonitor options) : - base(logger, mediator, client, options) + protected override Task> OnGetWatcher(IKubernetes client, + CancellationToken cancellationToken) { - } - - - protected override Task> OnGetWatcher(CancellationToken cancellationToken) - { - return Client.CoreV1.ListConfigMapForAllNamespacesWithHttpMessagesAsync(watch: true, timeoutSeconds: WatcherTimeout, + return client.CoreV1.ListConfigMapForAllNamespacesWithHttpMessagesAsync(watch: true, + timeoutSeconds: WatcherTimeout, cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/Core/Extensions/MetadataExtensions.cs b/src/ES.Kubernetes.Reflector/Core/Extensions/MetadataExtensions.cs index a10891a..ae35a58 100644 --- a/src/ES.Kubernetes.Reflector/Core/Extensions/MetadataExtensions.cs +++ b/src/ES.Kubernetes.Reflector/Core/Extensions/MetadataExtensions.cs @@ -11,7 +11,7 @@ public static class MetadataExtensions public static IReadOnlyDictionary SafeAnnotations(this V1ObjectMeta metadata) { - return (IReadOnlyDictionary) (metadata.Annotations ?? new Dictionary()); + return (IReadOnlyDictionary)(metadata.Annotations ?? new Dictionary()); } public static KubeRef GetRef(this IKubernetesObject resource) @@ -39,11 +39,11 @@ public static bool TryGet(this IReadOnlyDictionary annotation Converters.TryAdd(typeof(T), conv); } - value = (T?) conv.ConvertFromString(raw.Trim()); + value = (T?)conv.ConvertFromString(raw.Trim()); } else { - value = (T) Convert.ChangeType(raw.Trim(), typeof(T)); + value = (T)Convert.ChangeType(raw.Trim(), typeof(T)); } return true; diff --git a/src/ES.Kubernetes.Reflector/Core/Json/JsonPropertyNameContractResolver.cs b/src/ES.Kubernetes.Reflector/Core/Json/JsonPropertyNameContractResolver.cs index ba0772e..e4abae5 100644 --- a/src/ES.Kubernetes.Reflector/Core/Json/JsonPropertyNameContractResolver.cs +++ b/src/ES.Kubernetes.Reflector/Core/Json/JsonPropertyNameContractResolver.cs @@ -18,6 +18,5 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ if (member.GetCustomAttribute() is not { } propertyNameAttribute) return property; property.PropertyName = propertyNameAttribute.Name; return property; - } } \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/Core/Mirroring/Extensions/ReflectionStatusExtensions.cs b/src/ES.Kubernetes.Reflector/Core/Mirroring/Extensions/ReflectionStatusExtensions.cs index 868c0a3..c0b51ec 100644 --- a/src/ES.Kubernetes.Reflector/Core/Mirroring/Extensions/ReflectionStatusExtensions.cs +++ b/src/ES.Kubernetes.Reflector/Core/Mirroring/Extensions/ReflectionStatusExtensions.cs @@ -20,7 +20,7 @@ public static bool CanBeAutoReflectedToNamespace(this ReflectorProperties proper private static bool PatternListMatch(string patternList, string value) { if (string.IsNullOrEmpty(patternList)) return true; - var regexPatterns = patternList.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries); + var regexPatterns = patternList.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); return regexPatterns.Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()) .Select(pattern => Regex.Match(value, pattern)) .Any(match => match.Success && match.Value.Length == value.Length); diff --git a/src/ES.Kubernetes.Reflector/Core/Mirroring/ResourceMirror.cs b/src/ES.Kubernetes.Reflector/Core/Mirroring/ResourceMirror.cs index 05e7c8f..4a11b47 100644 --- a/src/ES.Kubernetes.Reflector/Core/Mirroring/ResourceMirror.cs +++ b/src/ES.Kubernetes.Reflector/Core/Mirroring/ResourceMirror.cs @@ -16,7 +16,7 @@ namespace ES.Kubernetes.Reflector.Core.Mirroring; -public abstract class ResourceMirror : +public abstract class ResourceMirror(ILogger logger, IServiceProvider serviceProvider) : INotificationHandler, INotificationHandler where TResource : class, IKubernetesObject @@ -27,23 +27,14 @@ public abstract class ResourceMirror : private readonly ConcurrentDictionary _notFoundCache = new(); private readonly ConcurrentDictionary _propertiesCache = new(); - protected readonly IKubernetes Client; - protected readonly ILogger Logger; - - - protected ResourceMirror(ILogger logger, IKubernetes client) - { - Logger = logger; - Client = client; - } + protected readonly ILogger Logger = logger; /// - /// Handles notifications + /// Handles notifications /// public Task Handle(WatcherClosed notification, CancellationToken cancellationToken) { - //If not TResource or Namespace, not something this instance should handle if (notification.ResourceType != typeof(TResource) && notification.ResourceType != typeof(V1Namespace)) return Task.CompletedTask; @@ -60,9 +51,8 @@ public Task Handle(WatcherClosed notification, CancellationToken cancellationTok } /// - /// Handles notifications + /// Handles notifications /// - public async Task Handle(WatcherEvent notification, CancellationToken cancellationToken) { switch (notification.Item) @@ -82,37 +72,37 @@ public async Task Handle(WatcherEvent notification, CancellationToken cancellati { case WatchEventType.Added: case WatchEventType.Modified: - { - await HandleUpsert(resource, notification.Type, cancellationToken); - } + { + await HandleUpsert(resource, cancellationToken); + } break; case WatchEventType.Deleted: + { + _propertiesCache.Remove(itemRef, out _); + var properties = resource.GetReflectionProperties(); + + + if (!properties.IsReflection) + { + if (properties is { Allowed: true, AutoEnabled: true } && + _autoReflectionCache.TryGetValue(itemRef, out var reflectionList)) + foreach (var reflectionId in reflectionList.ToArray()) + { + Logger.LogDebug("Deleting {id} - Source {sourceId} has been deleted", reflectionId, + itemRef); + await OnResourceDelete(reflectionId); + } + + _autoSources.Remove(itemRef, out _); + _directReflectionCache.Remove(itemRef, out _); + _autoReflectionCache.Remove(itemRef, out _); + } + else { - _propertiesCache.Remove(itemRef, out _); - var properties = resource.GetReflectionProperties(); - - - if (!properties.IsReflection) - { - if (properties is { Allowed: true, AutoEnabled: true } && - _autoReflectionCache.TryGetValue(itemRef, out var reflectionList)) - foreach (var reflectionId in reflectionList.ToArray()) - { - Logger.LogDebug("Deleting {id} - Source {sourceId} has been deleted", reflectionId, - itemRef); - await OnResourceDelete(reflectionId); - } - - _autoSources.Remove(itemRef, out _); - _directReflectionCache.Remove(itemRef, out _); - _autoReflectionCache.Remove(itemRef, out _); - } - else - { - foreach (var item in _directReflectionCache) item.Value.Remove(itemRef); - foreach (var item in _autoReflectionCache) item.Value.Remove(itemRef); - } + foreach (var item in _directReflectionCache) item.Value.Remove(itemRef); + foreach (var item in _autoReflectionCache) item.Value.Remove(itemRef); } + } break; case WatchEventType.Error: case WatchEventType.Bookmark: @@ -122,35 +112,35 @@ public async Task Handle(WatcherEvent notification, CancellationToken cancellati break; case V1Namespace ns: - { - if (notification.Type != WatchEventType.Added) return; - Logger.LogTrace("Handling {eventType} {resourceType} {resourceRef}", notification.Type, ns.Kind, - ns.GetRef()); + { + if (notification.Type != WatchEventType.Added) return; + Logger.LogTrace("Handling {eventType} {resourceType} {resourceRef}", notification.Type, ns.Kind, + ns.GetRef()); - foreach (var autoSourceRef in _autoSources.Keys) - { - var properties = _propertiesCache[autoSourceRef]; - if (!properties.CanBeAutoReflectedToNamespace(ns.Name())) continue; + foreach (var autoSourceRef in _autoSources.Keys) + { + var properties = _propertiesCache[autoSourceRef]; + if (!properties.CanBeAutoReflectedToNamespace(ns.Name())) continue; - var reflectionRef = new KubeRef(ns.Name(), autoSourceRef.Name); - var autoReflectionList = _autoReflectionCache.GetOrAdd(autoSourceRef, new List()); + var reflectionRef = new KubeRef(ns.Name(), autoSourceRef.Name); + var autoReflectionList = _autoReflectionCache.GetOrAdd(autoSourceRef, new List()); - if (autoReflectionList.Contains(reflectionRef)) return; + if (autoReflectionList.Contains(reflectionRef)) return; - await ResourceReflect(autoSourceRef, reflectionRef, null, null, true); + await ResourceReflect(autoSourceRef, reflectionRef, null, null, true); - if (!autoReflectionList.Contains(reflectionRef)) - autoReflectionList.Add(reflectionRef); - } + if (!autoReflectionList.Contains(reflectionRef)) + autoReflectionList.Add(reflectionRef); } + } break; } } - private async Task HandleUpsert(TResource resource, WatchEventType eventType, CancellationToken cancellationToken) + private async Task HandleUpsert(TResource resource, CancellationToken cancellationToken) { var resourceRef = resource.GetRef(); var properties = resource.GetReflectionProperties(); @@ -187,13 +177,13 @@ private async Task HandleUpsert(TResource resource, WatchEventType eventType, Ca await OnResourceDelete(reflectionId); } - var isAutoSource = properties is { Allowed: true, AutoEnabled: true}; + var isAutoSource = properties is { Allowed: true, AutoEnabled: true }; //Update the status of an auto-source _autoSources.AddOrUpdate(resourceRef, isAutoSource, (_, _) => isAutoSource); //If not allowed or auto is disabled, remove the cache for auto-reflections - if (!isAutoSource) { _autoReflectionCache.Remove(resourceRef, out _); } + if (!isAutoSource) _autoReflectionCache.Remove(resourceRef, out _); //If reflection is disabled, remove the reflections cache and stop reflecting if (!properties.Allowed) @@ -207,7 +197,7 @@ private async Task HandleUpsert(TResource resource, WatchEventType eventType, Ca if (_directReflectionCache.TryGetValue(resourceRef, out reflectionList)) foreach (var reflectionRef in reflectionList.ToArray()) { - //Try to get the properties for the reflection. Otherwise remove it + //Try to get the properties for the reflection. Otherwise, remove it if (!_propertiesCache.TryGetValue(reflectionRef, out var reflectionProperties)) { reflectionList.Remove(reflectionRef); @@ -334,7 +324,8 @@ private async Task AutoReflectionForSource(KubeRef resourceRef, TResource? resou var autoReflectionList = _autoReflectionCache.GetOrAdd(resourceRef, _ => new List()); var matches = await OnResourceWithNameList(resourceRef.Name); - var namespaces = (await Client.CoreV1.ListNamespaceAsync(cancellationToken: cancellationToken)).Items; + using var client = serviceProvider.GetRequiredService(); + var namespaces = (await client.CoreV1.ListNamespaceAsync(cancellationToken: cancellationToken)).Items; foreach (var match in matches) { @@ -374,7 +365,6 @@ private async Task AutoReflectionForSource(KubeRef resourceRef, TResource? resou m.GetReflectionProperties().Reflects.Equals(resourceRef)) .Select(m => m.GetRef()).ToList(); - autoReflectionList.Clear(); autoReflectionList.AddRange(toCreate); diff --git a/src/ES.Kubernetes.Reflector/Core/NamespaceWatcher.cs b/src/ES.Kubernetes.Reflector/Core/NamespaceWatcher.cs index bb8c8be..47642df 100644 --- a/src/ES.Kubernetes.Reflector/Core/NamespaceWatcher.cs +++ b/src/ES.Kubernetes.Reflector/Core/NamespaceWatcher.cs @@ -8,18 +8,17 @@ namespace ES.Kubernetes.Reflector.Core; -public class NamespaceWatcher : WatcherBackgroundService +public class NamespaceWatcher( + ILogger logger, + IMediator mediator, + IServiceProvider serviceProvider, + IOptionsMonitor options) + : WatcherBackgroundService(logger, mediator, serviceProvider, options) { - public NamespaceWatcher(ILogger logger, IMediator mediator, IKubernetes client, - IOptionsMonitor options) : - base(logger, mediator, client, options) + protected override Task> OnGetWatcher(IKubernetes client, + CancellationToken cancellationToken) { - } - - - protected override Task> OnGetWatcher(CancellationToken cancellationToken) - { - return Client.CoreV1.ListNamespaceWithHttpMessagesAsync(watch: true, timeoutSeconds: WatcherTimeout, + return client.CoreV1.ListNamespaceWithHttpMessagesAsync(watch: true, timeoutSeconds: WatcherTimeout, cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/Core/Resources/KubeRef.cs b/src/ES.Kubernetes.Reflector/Core/Resources/KubeRef.cs index 2ee5836..fb4648e 100644 --- a/src/ES.Kubernetes.Reflector/Core/Resources/KubeRef.cs +++ b/src/ES.Kubernetes.Reflector/Core/Resources/KubeRef.cs @@ -15,7 +15,7 @@ public KubeRef(string ns, string name) public KubeRef(string value) { if (string.IsNullOrWhiteSpace(value)) return; - var split = value.Split(new[] {"/"}, StringSplitOptions.RemoveEmptyEntries).ToList(); + var split = value.Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries).ToList(); if (!split.Any()) return; switch (split.Count) { @@ -39,12 +39,19 @@ public KubeRef(V1ObjectMeta metadata) : this(metadata.Namespace() ?? string.Empt public string Name { get; } = string.Empty; + public bool Equals(KubeRef? other) + { + if (ReferenceEquals(this, other)) return true; + return string.Equals(Namespace, other?.Namespace) && string.Equals(Name, other?.Name); + } + + public static bool TryParse(string value, out KubeRef id) { id = Empty; if (string.IsNullOrWhiteSpace(value)) return false; - var split = value.Trim().Split(new[] {"/"}, StringSplitOptions.RemoveEmptyEntries).ToList(); + var split = value.Trim().Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries).ToList(); if (!split.Any()) return false; if (split.Count > 2) return false; id = split.Count == 1 @@ -55,15 +62,6 @@ public static bool TryParse(string value, out KubeRef id) } - public bool Equals(KubeRef? other) - { - if (ReferenceEquals(this, other)) return true; - return string.Equals(Namespace, other?.Namespace) && string.Equals(Name, other?.Name); - } - - - - public override int GetHashCode() { unchecked diff --git a/src/ES.Kubernetes.Reflector/Core/SecretMirror.cs b/src/ES.Kubernetes.Reflector/Core/SecretMirror.cs index f31aa95..f875915 100644 --- a/src/ES.Kubernetes.Reflector/Core/SecretMirror.cs +++ b/src/ES.Kubernetes.Reflector/Core/SecretMirror.cs @@ -6,21 +6,23 @@ namespace ES.Kubernetes.Reflector.Core; -public class SecretMirror : ResourceMirror +public class SecretMirror(ILogger logger, IServiceProvider serviceProvider) + : ResourceMirror(logger, serviceProvider) { - public SecretMirror(ILogger logger, IKubernetes client) : base(logger, client) - { - } + private readonly IServiceProvider _serviceProvider = serviceProvider; protected override async Task OnResourceWithNameList(string itemRefName) { - return (await Client.CoreV1.ListSecretForAllNamespacesAsync(fieldSelector: $"metadata.name={itemRefName}")).Items + using var client = _serviceProvider.GetRequiredService(); + return (await client.CoreV1.ListSecretForAllNamespacesAsync(fieldSelector: $"metadata.name={itemRefName}")) + .Items .ToArray(); } - protected override Task OnResourceApplyPatch(V1Patch patch, KubeRef refId) + protected override async Task OnResourceApplyPatch(V1Patch patch, KubeRef refId) { - return Client.CoreV1.PatchNamespacedSecretWithHttpMessagesAsync(patch, refId.Name, refId.Namespace); + using var client = _serviceProvider.GetRequiredService(); + await client.CoreV1.PatchNamespacedSecretWithHttpMessagesAsync(patch, refId.Name, refId.Namespace); } protected override Task OnResourceConfigurePatch(V1Secret source, JsonPatchDocument patchDoc) @@ -29,9 +31,10 @@ protected override Task OnResourceConfigurePatch(V1Secret source, JsonPatchDocum return Task.CompletedTask; } - protected override Task OnResourceCreate(V1Secret item, string ns) + protected override async Task OnResourceCreate(V1Secret item, string ns) { - return Client.CoreV1.CreateNamespacedSecretAsync(item, ns); + using var client = _serviceProvider.GetRequiredService(); + await client.CoreV1.CreateNamespacedSecretAsync(item, ns); } protected override Task OnResourceClone(V1Secret sourceResource) @@ -45,14 +48,16 @@ protected override Task OnResourceClone(V1Secret sourceResource) }); } - protected override Task OnResourceDelete(KubeRef resourceId) + protected override async Task OnResourceDelete(KubeRef resourceId) { - return Client.CoreV1.DeleteNamespacedSecretAsync(resourceId.Name, resourceId.Namespace); + using var client = _serviceProvider.GetRequiredService(); + await client.CoreV1.DeleteNamespacedSecretAsync(resourceId.Name, resourceId.Namespace); } - protected override Task OnResourceGet(KubeRef refId) + protected override async Task OnResourceGet(KubeRef refId) { - return Client.CoreV1.ReadNamespacedSecretAsync(refId.Name, refId.Namespace); + using var client = _serviceProvider.GetRequiredService(); + return await client.CoreV1.ReadNamespacedSecretAsync(refId.Name, refId.Namespace); } protected override Task OnResourceIgnoreCheck(V1Secret item) diff --git a/src/ES.Kubernetes.Reflector/Core/SecretWatcher.cs b/src/ES.Kubernetes.Reflector/Core/SecretWatcher.cs index 9c5a13c..cc8995d 100644 --- a/src/ES.Kubernetes.Reflector/Core/SecretWatcher.cs +++ b/src/ES.Kubernetes.Reflector/Core/SecretWatcher.cs @@ -8,18 +8,18 @@ namespace ES.Kubernetes.Reflector.Core; -public class SecretWatcher : WatcherBackgroundService +public class SecretWatcher( + ILogger logger, + IMediator mediator, + IServiceProvider serviceProvider, + IOptionsMonitor options) + : WatcherBackgroundService(logger, mediator, serviceProvider, options) { - public SecretWatcher(ILogger logger, IMediator mediator, IKubernetes client, - IOptionsMonitor options) : - base(logger, mediator, client, options) + protected override Task> OnGetWatcher(IKubernetes client, + CancellationToken cancellationToken) { - } - - - protected override Task> OnGetWatcher(CancellationToken cancellationToken) - { - return Client.CoreV1.ListSecretForAllNamespacesWithHttpMessagesAsync(watch: true, timeoutSeconds: WatcherTimeout, + return client.CoreV1.ListSecretForAllNamespacesWithHttpMessagesAsync(watch: true, + timeoutSeconds: WatcherTimeout, cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/Core/Watchers/WatcherBackgroundService.cs b/src/ES.Kubernetes.Reflector/Core/Watchers/WatcherBackgroundService.cs index 095e453..bbe1b0d 100644 --- a/src/ES.Kubernetes.Reflector/Core/Watchers/WatcherBackgroundService.cs +++ b/src/ES.Kubernetes.Reflector/Core/Watchers/WatcherBackgroundService.cs @@ -9,41 +9,43 @@ namespace ES.Kubernetes.Reflector.Core.Watchers; -public abstract class WatcherBackgroundService : BackgroundService +public abstract class WatcherBackgroundService( + ILogger logger, + IMediator mediator, + IServiceProvider serviceProvider, + IOptionsMonitor options) + : BackgroundService where TResource : IKubernetesObject { - private readonly IOptionsMonitor _options; - protected readonly IKubernetes Client; - protected readonly ILogger Logger; - protected readonly IMediator Mediator; + protected readonly ILogger Logger = logger; + protected readonly IMediator Mediator = mediator; - protected WatcherBackgroundService(ILogger logger, IMediator mediator, IKubernetes client, - IOptionsMonitor options) - { - Logger = logger; - Mediator = mediator; - Client = client; - _options = options; - } - - protected int? WatcherTimeout => _options.CurrentValue.Watcher?.Timeout; + protected int WatcherTimeout => options.CurrentValue.Watcher?.Timeout ?? 3600; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var sessionStopwatch = new Stopwatch(); while (!stoppingToken.IsCancellationRequested) { + await using var scope = serviceProvider.CreateAsyncScope(); + var sessionFaulted = false; sessionStopwatch.Restart(); + try { Logger.LogInformation("Requesting {type} resources", typeof(TResource).Name); - using var watcher = OnGetWatcher(stoppingToken); - var watchList = watcher.WatchAsync(cancellationToken: stoppingToken); - await foreach (var (type, item) in watchList - .WithCancellation(stoppingToken)) + + using var absoluteTimeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(WatcherTimeout + 3)); + using var cancellationCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, absoluteTimeoutCts.Token); + using var client = scope.ServiceProvider.GetRequiredService(); + + using var watcher = OnGetWatcher(client, stoppingToken); + var watchList = watcher.WatchAsync(cancellationToken: cancellationCts.Token); + + await foreach (var (type, item) in watchList) await Mediator.Publish(new WatcherEvent { Item = item, @@ -77,5 +79,6 @@ await Mediator.Publish(new WatcherClosed } } - protected abstract Task> OnGetWatcher(CancellationToken cancellationToken); + protected abstract Task> OnGetWatcher(IKubernetes client, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/Dockerfile b/src/ES.Kubernetes.Reflector/Dockerfile index ca12d46..b657cfe 100644 --- a/src/ES.Kubernetes.Reflector/Dockerfile +++ b/src/ES.Kubernetes.Reflector/Dockerfile @@ -1,8 +1,9 @@ -FROM mcr.microsoft.com/dotnet/aspnet:7.0-bookworm-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim AS base +USER app WORKDIR /app -EXPOSE 25080 +EXPOSE 8080 -FROM mcr.microsoft.com/dotnet/sdk:7.0-bookworm-slim-amd64 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim-amd64 AS build WORKDIR /src COPY ["ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj", "ES.Kubernetes.Reflector/"] RUN dotnet restore "ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj" diff --git a/src/ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj b/src/ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj index e4146be..52051b0 100644 --- a/src/ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj +++ b/src/ES.Kubernetes.Reflector/ES.Kubernetes.Reflector.csproj @@ -1,7 +1,7 @@ - + - net7.0 + net9.0 enable enable Linux @@ -9,15 +9,13 @@ - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/Program.cs b/src/ES.Kubernetes.Reflector/Program.cs index 5cae923..08f140f 100644 --- a/src/ES.Kubernetes.Reflector/Program.cs +++ b/src/ES.Kubernetes.Reflector/Program.cs @@ -1,103 +1,53 @@ -using Autofac; -using Autofac.Extensions.DependencyInjection; +using ES.FX.Hosting.Lifetime; +using ES.FX.Ignite.Hosting; +using ES.FX.Ignite.OpenTelemetry.Exporter.Seq.Hosting; +using ES.FX.Ignite.Serilog.Hosting; +using ES.FX.Serilog.Lifetime; using ES.Kubernetes.Reflector.Core; using ES.Kubernetes.Reflector.Core.Configuration; using k8s; using Microsoft.Extensions.Options; -using Serilog; -Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("reflector.logging.json") - .AddEnvironmentVariables("ES_") - .AddCommandLine(args) - .Build()) - .CreateLogger(); - - -try +return await ProgramEntry.CreateBuilder(args).UseSerilog().Build().RunAsync(async _ => { - Log.Information("Starting host"); - var builder = WebApplication.CreateBuilder(args); - builder.Environment.EnvironmentName = - Environment.GetEnvironmentVariable($"{nameof(ES)}_{nameof(Environment)}") ?? - Environments.Production; + builder.Logging.ClearProviders(); - builder.Configuration.AddJsonFile("appsettings.json", false, true); - builder.Configuration.AddJsonFile("reflector.logging.json"); builder.Configuration.AddEnvironmentVariables("ES_"); - builder.Configuration.AddCommandLine(args); - - builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); - builder.Host.UseSerilog(); - builder.Host.UseConsoleLifetime(); - - builder.Services.AddHttpClient(); - builder.Services.AddOptions(); - builder.Services.AddHealthChecks(); - builder.Services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(void).Assembly)); - builder.Services.AddControllers(); + builder.Ignite(); + builder.IgniteSerilog(); + builder.IgniteSeqOpenTelemetryExporter(); + builder.Services.AddMediatR(config => + config.RegisterServicesFromAssembly(typeof(Program).Assembly)); - builder.Services.Configure(builder.Configuration.GetSection("Reflector")); + builder.Services.Configure(builder.Configuration.GetSection(nameof(ES.Kubernetes.Reflector))); - - builder.Services.AddSingleton(s => + builder.Services.AddTransient(s => { var reflectorOptions = s.GetRequiredService>(); var config = KubernetesClientConfiguration.BuildDefaultConfig(); - config.HttpClientTimeout = TimeSpan.FromMinutes(30); if (reflectorOptions.Value.Kubernetes is not null) - { - config.SkipTlsVerify = + config.SkipTlsVerify = reflectorOptions.Value.Kubernetes.SkipTlsVerify.GetValueOrDefault(false); - } return config; }); - - - builder.Services.AddSingleton(s => - new Kubernetes(s.GetRequiredService())); + builder.Services.AddTransient(s => + new Kubernetes(s.GetRequiredService())); - builder.Host.ConfigureContainer((ContainerBuilder container) => - { - container.Register(c => c.Resolve().CreateClient()).AsSelf(); - - container.RegisterType().AsImplementedInterfaces().SingleInstance(); - - container.RegisterType().AsImplementedInterfaces().SingleInstance(); - container.RegisterType().AsImplementedInterfaces().SingleInstance(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + builder.Services.AddHostedService(); - container.RegisterType().AsImplementedInterfaces().SingleInstance(); - container.RegisterType().AsImplementedInterfaces().SingleInstance(); - }); - - builder.WebHost.ConfigureKestrel(options => { options.ListenAnyIP(25080); }); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); var app = builder.Build(); - - if (!app.Environment.IsDevelopment()) app.UseExceptionHandler("/Error"); - - app.UseStaticFiles(); - app.UseRouting(); - app.UseHealthChecks("/healthz"); - app.UseAuthorization(); - + app.Ignite(); await app.RunAsync(); return 0; -} -catch (Exception ex) -{ - Log.Fatal(ex, "Host terminated unexpectedly"); - return 1; -} -finally -{ - Log.CloseAndFlush(); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/Properties/launchSettings.json b/src/ES.Kubernetes.Reflector/Properties/launchSettings.json index 721ffa5..8df3f75 100644 --- a/src/ES.Kubernetes.Reflector/Properties/launchSettings.json +++ b/src/ES.Kubernetes.Reflector/Properties/launchSettings.json @@ -4,9 +4,9 @@ "commandName": "Project", "launchBrowser": false, "environmentVariables": { - "ES_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://0.0.0.0:25080", + "applicationUrl": "http://0.0.0.0:8080", "dotnetRunMessages": false }, "Docker": { diff --git a/src/ES.Kubernetes.Reflector/appsettings.Development.json b/src/ES.Kubernetes.Reflector/appsettings.Development.json index e47ef4b..aaae1f7 100644 --- a/src/ES.Kubernetes.Reflector/appsettings.Development.json +++ b/src/ES.Kubernetes.Reflector/appsettings.Development.json @@ -1,3 +1,23 @@ { - "DetailedErrors": true + "Serilog": { + "MinimumLevel": { + "Override": { + "ES.Kubernetes": "Verbose" + } + } + }, + + "Ignite": { + "OpenTelemetry": { + "Exporter": { + "Seq": { + "IngestionEndpoint": "http://seq.localenv.io:5341", + "HealthUrl": "http://seq.localenv.io/health", + "Settings": { + "Enabled": true + } + } + } + } + } } \ No newline at end of file diff --git a/src/ES.Kubernetes.Reflector/appsettings.json b/src/ES.Kubernetes.Reflector/appsettings.json index 383f7fa..2864055 100644 --- a/src/ES.Kubernetes.Reflector/appsettings.json +++ b/src/ES.Kubernetes.Reflector/appsettings.json @@ -1,5 +1,50 @@ { - "AllowedHosts": "*", + "Serilog": { + "Using": [ "Serilog.Sinks.Seq" ], + "LevelSwitches": { "$consoleLevelSwitch": "Verbose" }, + "MinimumLevel": { + "Default": "Verbose", + "Override": { + "Microsoft": "Information", + "System.Net.Http": "Warning", + "Polly": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.DataProtection": "Error", + "ES.FX": "Information", + "ES.Kubernetes.Reflector": "Information" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "levelSwitch": "$consoleLevelSwitch" + } + } + ] + }, + "Ignite": { + "Settings": { + "Configuration": { + "AdditionalJsonSettingsFiles": [], + "AdditionalJsonAppSettingsOverrides": [ "overrides" ] + }, + "OpenTelemetry": { + "AspNetCoreTracingHealthChecksRequestsFiltered": true + } + }, + "OpenTelemetry": { + "Exporter": { + "Seq": { + "Settings": { + "Enabled": false + } + } + } + } + }, "Reflector": { "Watcher": { "Timeout": "" diff --git a/src/ES.Kubernetes.Reflector/reflector.logging.json b/src/ES.Kubernetes.Reflector/reflector.logging.json deleted file mode 100644 index 0e768f6..0000000 --- a/src/ES.Kubernetes.Reflector/reflector.logging.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "Serilog": { - "Using": ["Serilog.Sinks.Console"], - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft.AspNetCore.Server.Kestrel": "Error", - "Microsoft": "Warning", - "System": "Warning", - "Microsoft.Extensions.Http": "Warning" - } - }, - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": - "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}" - } - } - ] - } -} \ No newline at end of file diff --git a/src/NuGet.config b/src/NuGet.config new file mode 100644 index 0000000..492986b --- /dev/null +++ b/src/NuGet.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/helm/reflector/templates/cron.yaml b/src/helm/reflector/templates/cron.yaml index 34ef812..b96dcad 100644 --- a/src/helm/reflector/templates/cron.yaml +++ b/src/helm/reflector/templates/cron.yaml @@ -68,6 +68,11 @@ spec: value: {{ .Values.configuration.logging.minimumLevel | quote }} - name: ES_Reflector__Watcher__Timeout value: {{ .Values.configuration.watcher.timeout | quote }} + - name: ES_Reflector__Kubernetes__SkipTlsVerify + value: {{ .Values.configuration.kubernetes.skipTlsVerify | quote }} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} resources: {{- toYaml .Values.resources | nindent 16 }} {{- end }} diff --git a/src/helm/reflector/templates/deployment.yaml b/src/helm/reflector/templates/deployment.yaml index d6c1dc6..7085512 100644 --- a/src/helm/reflector/templates/deployment.yaml +++ b/src/helm/reflector/templates/deployment.yaml @@ -57,21 +57,15 @@ spec: ports: - name: http - containerPort: 25080 + containerPort: 8080 protocol: TCP livenessProbe: - {{- toYaml .Values.healthcheck | nindent 12 }} - initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.livenessProbe.periodSeconds }} + {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: - {{- toYaml .Values.healthcheck | nindent 12 }} - initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} - periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + {{- toYaml .Values.readinessProbe | nindent 12 }} {{- if semverCompare ">= 1.18-0" .Capabilities.KubeVersion.Version }} startupProbe: - {{- toYaml .Values.healthcheck | nindent 12 }} - failureThreshold: {{ .Values.startupProbe.failureThreshold }} - periodSeconds: {{ .Values.startupProbe.periodSeconds }} + {{- toYaml .Values.startupProbe | nindent 12 }} {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} @@ -89,7 +83,7 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} - {{ - with .Values.volumes }} + {{- with .Values.volumes }} volumes: {{- toYaml . | nindent 8}} {{- end }} diff --git a/src/helm/reflector/values.yaml b/src/helm/reflector/values.yaml index b4da1b4..14f9b78 100644 --- a/src/helm/reflector/values.yaml +++ b/src/helm/reflector/values.yaml @@ -61,21 +61,31 @@ securityContext: runAsNonRoot: true runAsUser: 1000 -healthcheck: +livenessProbe: httpGet: - path: /healthz + path: /health/live port: http - -livenessProbe: + timeoutSeconds: 10 initialDelaySeconds: 5 periodSeconds: 10 + failureThreshold: 5 readinessProbe: + httpGet: + path: /health/ready + port: http + timeoutSeconds: 10 initialDelaySeconds: 5 periodSeconds: 10 + failureThreshold: 5 startupProbe: - # The application will have a maximum of 50s (10 * 5 = 50s) to finish its startup. - failureThreshold: 10 - periodSeconds: 5 + httpGet: + path: /health/ready + port: http + timeoutSeconds: 10 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 5 + resources: {} @@ -107,11 +117,6 @@ topologySpreadConstraints: [] priorityClassName: "" -#mount external persistent/ephemeral storage to required locations if readOnlyRootFileSystem=true -volumes: - - name: tmp - emptyDir: {} +volumes: [] -volumeMounts: - - name: tmp - mountPath: /tmp \ No newline at end of file +volumeMounts: [] \ No newline at end of file