From 31bfc6822eeebc8d68c07894097fc936b3cb6145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20BENOIST?= Date: Wed, 19 Feb 2025 17:13:48 +0100 Subject: [PATCH] Allow to remap key name when mirroring secret Use reflector.v1.k8s.emberstack.com/reflection-key-mapping and reflector.v1.k8s.emberstack.com/reflection-auto-key-mapping to do the renaming See README.md for details --- README.md | 33 ++++++++++ .../Core/ConfigMapMirror.cs | 12 ++-- .../Core/Mirroring/Constants/Annotations.cs | 2 + .../Extensions/ReflectorExtensions.cs | 10 +++ .../Core/Mirroring/ReflectorProperties.cs | 2 + .../Core/Mirroring/ResourceMirror.cs | 64 ++++++++++++++++--- .../Core/SecretMirror.cs | 9 +-- 7 files changed, 113 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2db9246..0748e84 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ $ kubectl -n kube-system apply -f https://github.com/emberstack/kubernetes-refle Reflector can create mirrors with the same name in other namespaces automatically. The following annotations control if and how the mirrors are created: - Add `reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"` to the resource annotations to automatically create mirrors in other namespaces. Note: Requires `reflector.v1.k8s.emberstack.com/reflection-allowed` to be `true` since mirrors need to able to reflect the source. - Add `reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: ""` to the resource annotations specify in which namespaces to automatically create mirrors. Note: If this annotation is omitted or is empty, all namespaces are allowed. Namespaces in this list will also be checked by `reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces` since mirrors need to be in namespaces from where reflection is permitted. + - Optionally add `reflector.v1.k8s.emberstack.com/reflection-auto-key-mapping: ""` to the resource annotations specify in the key mapping to use when automatically create mirrors. Expected format is a list of comma separated `src_key:dst_key`. > Important: If the `source` is deleted, automatic mirrors are deleted. Also if either reflection or automirroring is turned off or the automatic mirror's namespace is no longer a valid match for the allowed namespaces, the automatic mirror is deleted. @@ -109,10 +110,29 @@ $ kubectl -n kube-system apply -f https://github.com/emberstack/kubernetes-refle data: ... ``` + + Example source secret with key mapping: + ```yaml + apiVersion: v1 + kind: Secret + metadata: + name: source-secret + annotations: + reflector.v1.k8s.emberstack.com/reflection-allowed: "true" + reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "namespace-1,namespace-2,namespace-[0-9]*" + reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true" + reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "namespace-1" + reflector.v1.k8s.emberstack.com/reflection-auto-key-mapping: "user:username,pass:password" + data: + user: YWRtaW4K + pass: YWRtaW4K + ... + ``` ### 2. Annotate the mirror secret or configmap - Add `reflector.v1.k8s.emberstack.com/reflects: "/"` to the mirror object. The value of the annotation is the full name of the source object in `namespace/name` format. + - Optionally add `reflector.v1.k8s.emberstack.com/reflection-key-mapping: ""` to the resource annotations specify in the key mapping to use when mirroring. Expected format is a list of comma separated `src_key:dst_key`. All omitted source key will be copied as is. No warning will be issued if the source key does not exist. > Note: Add `reflector.v1.k8s.emberstack.com/reflected-version: ""` to the resource annotations when doing any manual changes to the mirror (for example when deploying with `helm` or re-applying the deployment script). This will reset the reflected version of the mirror. @@ -140,6 +160,19 @@ $ kubectl -n kube-system apply -f https://github.com/emberstack/kubernetes-refle ... ``` + Example mirror secret with key mapping : + ```yaml + apiVersion: v1 + kind: Secret + metadata: + name: mirror-secret + annotations: + reflector.v1.k8s.emberstack.com/reflects: "default/source-secret" + reflector.v1.k8s.emberstack.com/reflection-key-mapping: "user:username,pass:password" + data: + ... + ``` + ### 3. Done! Reflector will monitor any changes done to the source objects and copy the following fields: - `data` for secrets diff --git a/src/ES.Kubernetes.Reflector/Core/ConfigMapMirror.cs b/src/ES.Kubernetes.Reflector/Core/ConfigMapMirror.cs index 430631e..e8f23d1 100644 --- a/src/ES.Kubernetes.Reflector/Core/ConfigMapMirror.cs +++ b/src/ES.Kubernetes.Reflector/Core/ConfigMapMirror.cs @@ -23,10 +23,10 @@ protected override Task OnResourceApplyPatch(V1Patch patch, KubeRef refId) return Client.CoreV1.PatchNamespacedConfigMapAsync(patch, refId.Name, refId.Namespace); } - protected override Task OnResourceConfigurePatch(V1ConfigMap source, JsonPatchDocument patchDoc) + protected override Task OnResourceConfigurePatch(V1ConfigMap source, JsonPatchDocument patchDoc, Dictionary? mapping) { - patchDoc.Replace(e => e.Data, source.Data); - patchDoc.Replace(e => e.BinaryData, source.BinaryData); + patchDoc.Replace(e => e.Data, MappedData(source.Data, mapping)); + patchDoc.Replace(e => e.BinaryData, MappedData(source.BinaryData, mapping)); return Task.CompletedTask; } @@ -35,14 +35,14 @@ protected override Task OnResourceCreate(V1ConfigMap item, string ns) return Client.CoreV1.CreateNamespacedConfigMapAsync(item, ns); } - protected override Task OnResourceClone(V1ConfigMap sourceResource) + protected override Task OnResourceClone(V1ConfigMap sourceResource, Dictionary? mapping) { return Task.FromResult(new V1ConfigMap { ApiVersion = sourceResource.ApiVersion, Kind = sourceResource.Kind, - Data = sourceResource.Data, - BinaryData = sourceResource.BinaryData + Data = MappedData(sourceResource.Data, mapping), + BinaryData = MappedData(sourceResource.BinaryData, mapping) }); } diff --git a/src/ES.Kubernetes.Reflector/Core/Mirroring/Constants/Annotations.cs b/src/ES.Kubernetes.Reflector/Core/Mirroring/Constants/Annotations.cs index b58a357..efa9b65 100644 --- a/src/ES.Kubernetes.Reflector/Core/Mirroring/Constants/Annotations.cs +++ b/src/ES.Kubernetes.Reflector/Core/Mirroring/Constants/Annotations.cs @@ -10,6 +10,8 @@ public static class Reflection public static string AllowedNamespaces => $"{Prefix}/reflection-allowed-namespaces"; public static string AutoEnabled => $"{Prefix}/reflection-auto-enabled"; public static string AutoNamespaces => $"{Prefix}/reflection-auto-namespaces"; + public static string KeyMapping => $"{Prefix}/reflection-key-mapping"; + public static string AutoKeyMapping => $"{Prefix}/reflection-auto-key-mapping"; public static string Reflects => $"{Prefix}/reflects"; diff --git a/src/ES.Kubernetes.Reflector/Core/Mirroring/Extensions/ReflectorExtensions.cs b/src/ES.Kubernetes.Reflector/Core/Mirroring/Extensions/ReflectorExtensions.cs index 0f4bb5b..2d8a1f6 100644 --- a/src/ES.Kubernetes.Reflector/Core/Mirroring/Extensions/ReflectorExtensions.cs +++ b/src/ES.Kubernetes.Reflector/Core/Mirroring/Extensions/ReflectorExtensions.cs @@ -35,6 +35,16 @@ public static ReflectorProperties GetReflectionProperties(this V1ObjectMeta meta ? autoNamespaces ?? string.Empty : string.Empty, + KeyMapping = metadata.SafeAnnotations() + .TryGet(Annotations.Reflection.KeyMapping, out string? keyMapping) + ? keyMapping ?? string.Empty + : string.Empty, + + AutoKeyMapping = metadata.SafeAnnotations() + .TryGet(Annotations.Reflection.AutoKeyMapping, out string? autoKeyMapping) + ? autoKeyMapping ?? string.Empty + : string.Empty, + Reflects = metadata.SafeAnnotations() .TryGet(Annotations.Reflection.Reflects, out string? metaReflects) ? string.IsNullOrWhiteSpace(metaReflects) ? KubeRef.Empty : diff --git a/src/ES.Kubernetes.Reflector/Core/Mirroring/ReflectorProperties.cs b/src/ES.Kubernetes.Reflector/Core/Mirroring/ReflectorProperties.cs index 93407e0..68f29ae 100644 --- a/src/ES.Kubernetes.Reflector/Core/Mirroring/ReflectorProperties.cs +++ b/src/ES.Kubernetes.Reflector/Core/Mirroring/ReflectorProperties.cs @@ -9,6 +9,8 @@ public class ReflectorProperties public bool AutoEnabled { get; set; } public string AutoNamespaces { get; set; } = string.Empty; public KubeRef Reflects { get; set; } = KubeRef.Empty; + public string KeyMapping { get; set; } = string.Empty; + public string AutoKeyMapping { get; set; } = string.Empty; public string Version { get; set; } = string.Empty; diff --git a/src/ES.Kubernetes.Reflector/Core/Mirroring/ResourceMirror.cs b/src/ES.Kubernetes.Reflector/Core/Mirroring/ResourceMirror.cs index 05e7c8f..3a35a16 100644 --- a/src/ES.Kubernetes.Reflector/Core/Mirroring/ResourceMirror.cs +++ b/src/ES.Kubernetes.Reflector/Core/Mirroring/ResourceMirror.cs @@ -149,6 +149,19 @@ public async Task Handle(WatcherEvent notification, CancellationToken cancellati } } + protected IDictionary MappedData(IDictionary? data, Dictionary? mapping = null) + { + mapping ??= new Dictionary(); + IDictionary newData = new Dictionary(); + if (data != null) + { + foreach (var (key, value) in data) + { + newData.Add(mapping.GetValueOrDefault(key, key), value); + } + } + return newData; + } private async Task HandleUpsert(TResource resource, WatchEventType eventType, CancellationToken cancellationToken) { @@ -428,13 +441,31 @@ private async Task ResourceReflect(KubeRef sourceId, KubeRef targetId, TResource [Annotations.Reflection.MetaReflectedVersion] = source.Metadata.ResourceVersion, [Annotations.Reflection.MetaReflectedAt] = JsonConvert.SerializeObject(DateTimeOffset.UtcNow) }; + patchAnnotations[Annotations.Reflection.KeyMapping] = ""; + if (autoReflection) + { + patchAnnotations[Annotations.Reflection.KeyMapping] = autoReflection ? source.GetReflectionProperties().AutoKeyMapping : string.Empty; + } else if (targetResource is not null) + { + patchAnnotations[Annotations.Reflection.KeyMapping] = + targetResource.Metadata.Annotations.TryGetValue(Annotations.Reflection.KeyMapping, out var keyMapping) ? keyMapping : string.Empty; + } + Dictionary mapping = new Dictionary(); + try + { + mapping = Mapping(patchAnnotations[Annotations.Reflection.KeyMapping]); + } + catch (FormatException e) + { + Logger.LogError(e, e.Message); + } try { if (targetResource is null) { - var newResource = await OnResourceClone(source); + var newResource = await OnResourceClone(source, mapping); newResource.Metadata ??= new V1ObjectMeta(); newResource.Metadata.Name = targetId.Name; newResource.Metadata.NamespaceProperty = targetId.Namespace; @@ -442,11 +473,6 @@ private async Task ResourceReflect(KubeRef sourceId, KubeRef targetId, TResource var newResourceAnnotations = newResource.Metadata.Annotations; foreach (var patchAnnotation in patchAnnotations) newResourceAnnotations[patchAnnotation.Key] = patchAnnotation.Value; - newResourceAnnotations[Annotations.Reflection.MetaAutoReflects] = autoReflection.ToString(); - newResourceAnnotations[Annotations.Reflection.Reflects] = sourceId.ToString(); - newResourceAnnotations[Annotations.Reflection.MetaReflectedVersion] = source.Metadata.ResourceVersion; - newResourceAnnotations[Annotations.Reflection.MetaReflectedAt] = - JsonConvert.SerializeObject(DateTimeOffset.UtcNow); try { @@ -476,7 +502,7 @@ private async Task ResourceReflect(KubeRef sourceId, KubeRef targetId, TResource annotations[patchAnnotation.Key] = patchAnnotation.Value; patchDoc.Replace(e => e.Metadata.Annotations, annotations); - await OnResourceConfigurePatch(source, patchDoc); + await OnResourceConfigurePatch(source, patchDoc, mapping); var patch = JsonConvert.SerializeObject(patchDoc, Formatting.Indented); await OnResourceApplyPatch(new V1Patch(patch, V1Patch.PatchType.JsonPatch), targetId); @@ -488,11 +514,31 @@ private async Task ResourceReflect(KubeRef sourceId, KubeRef targetId, TResource } } + private Dictionary Mapping(string? keyMapping) + { + var mappings = new Dictionary(); + if (string.IsNullOrEmpty(keyMapping)) return mappings; + + foreach (var definition in keyMapping.Split(",")) + { + var definitionDetail = definition.Split(":"); + if (definitionDetail.Length == 2) + { + mappings.Add(definitionDetail[0], definitionDetail[1]); + } + else + { + throw new FormatException("Invalid key mapping, format is src_key:dst_key,src_other:dst_other. Received: " + definition); + } + } + return mappings; + } + protected abstract Task OnResourceApplyPatch(V1Patch source, KubeRef refId); - protected abstract Task OnResourceConfigurePatch(TResource source, JsonPatchDocument patchDoc); + protected abstract Task OnResourceConfigurePatch(TResource source, JsonPatchDocument patchDoc, Dictionary mapping); protected abstract Task OnResourceCreate(TResource item, string ns); - protected abstract Task OnResourceClone(TResource sourceResource); + protected abstract Task OnResourceClone(TResource sourceResource, Dictionary mapping); protected abstract Task OnResourceDelete(KubeRef resourceId); diff --git a/src/ES.Kubernetes.Reflector/Core/SecretMirror.cs b/src/ES.Kubernetes.Reflector/Core/SecretMirror.cs index f31aa95..b3b553a 100644 --- a/src/ES.Kubernetes.Reflector/Core/SecretMirror.cs +++ b/src/ES.Kubernetes.Reflector/Core/SecretMirror.cs @@ -1,4 +1,5 @@ using ES.Kubernetes.Reflector.Core.Mirroring; +using ES.Kubernetes.Reflector.Core.Mirroring.Extensions; using ES.Kubernetes.Reflector.Core.Resources; using k8s; using k8s.Models; @@ -23,9 +24,9 @@ protected override Task OnResourceApplyPatch(V1Patch patch, KubeRef refId) return Client.CoreV1.PatchNamespacedSecretWithHttpMessagesAsync(patch, refId.Name, refId.Namespace); } - protected override Task OnResourceConfigurePatch(V1Secret source, JsonPatchDocument patchDoc) + protected override Task OnResourceConfigurePatch(V1Secret source, JsonPatchDocument patchDoc, Dictionary mapping) { - patchDoc.Replace(e => e.Data, source.Data); + patchDoc.Replace(e => e.Data, MappedData(source.Data, mapping)); return Task.CompletedTask; } @@ -34,14 +35,14 @@ protected override Task OnResourceCreate(V1Secret item, string ns) return Client.CoreV1.CreateNamespacedSecretAsync(item, ns); } - protected override Task OnResourceClone(V1Secret sourceResource) + protected override Task OnResourceClone(V1Secret sourceResource, Dictionary mapping) { return Task.FromResult(new V1Secret { ApiVersion = sourceResource.ApiVersion, Kind = sourceResource.Kind, Type = sourceResource.Type, - Data = sourceResource.Data + Data = MappedData(sourceResource.Data, mapping) }); }