From eb9e87701da31ef2295c9deb57a7f9659a420644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sat, 26 Apr 2025 15:21:06 +0200 Subject: [PATCH] feat: encode and decode base64 in qute template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add sample secret copy to config map Signed-off-by: Attila Mészáros --- .../templating/GenericTemplateHandler.java | 36 ++++++++++++++- .../operator/glue/GlueTest.java | 45 +++++++++++++++++-- .../operator/glue/TestBase.java | 16 +++++-- .../resources/glue/CopySecretToConfigMap.yaml | 24 ++++++++++ 4 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 src/test/resources/glue/CopySecretToConfigMap.yaml diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java b/src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java index babe5ed..66c28f7 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.glue.templating; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.HashMap; import java.util.Map; @@ -10,6 +12,7 @@ import io.javaoperatorsdk.operator.glue.customresource.glue.Glue; import io.quarkus.qute.Engine; import io.quarkus.qute.Template; +import io.quarkus.qute.ValueResolver; import com.fasterxml.jackson.databind.ObjectMapper; @@ -21,7 +24,10 @@ public class GenericTemplateHandler { public static final String WORKFLOW_METADATA_KEY = "glueMetadata"; private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final Engine engine = Engine.builder().addDefaults().build(); + private static final Engine engine = Engine.builder().addDefaults() + .addValueResolver(base64EncodeResolver()) + .addValueResolver(base64DecodeResolver()) + .build(); public String processTemplate(Map> data, String template, boolean objectTemplate) { @@ -77,4 +83,32 @@ public String processTemplate(String template, Glue primary, boolean objectTempl return Serialization.unmarshal(template, Map.class); } + static ValueResolver base64DecodeResolver() { + return ValueResolver.builder() + .appliesTo(c -> c.getName().equals("decodeBase64") + && (c.getBase() instanceof String || c.getBase() instanceof byte[])) + .resolveSync(context -> { + if (context.getBase() instanceof byte[] bytes) { + return new String(Base64.getDecoder().decode(bytes), StandardCharsets.UTF_8); + } else { + return new String(Base64.getDecoder().decode(context.getBase().toString()), + StandardCharsets.UTF_8); + } + }).build(); + } + + static ValueResolver base64EncodeResolver() { + return ValueResolver.builder() + .appliesTo(c -> c.getName().equals("encodeBase64") + && (c.getBase() instanceof String || c.getBase() instanceof byte[])) + .applyToBaseClass(String.class) + .resolveSync(context -> { + if (context.getBase() instanceof byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } else { + return Base64.getEncoder().encodeToString(context.getBase().toString().getBytes()); + } + }).build(); + } + } diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java index 6564439..962f8bc 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java @@ -1,7 +1,9 @@ package io.javaoperatorsdk.operator.glue; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.stream.IntStream; @@ -23,6 +25,7 @@ import io.quarkus.test.junit.QuarkusTest; import static io.javaoperatorsdk.operator.glue.TestUtils.INITIAL_RECONCILE_WAIT_TIMEOUT; +import static io.javaoperatorsdk.operator.glue.TestUtils.loadGlue; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -412,6 +415,45 @@ void invalidGlueMessageHandling() { }); } + @Test + void secretToConfigMapCopy() { + String nsa = "namespace-a"; + String nsb = "namespace-b"; + + createNamespace(nsa); + createNamespace(nsb); + createSecretToCopy(nsb); + + var glue = client.resource(loadGlue("/glue/CopySecretToConfigMap.yaml")) + .createOr(NonDeletingOperation::update); + + await().untilAsserted(() -> { + var cm = client.configMaps().inNamespace(nsa).withName("my-secret-copy").get(); + assertThat(cm).isNotNull(); + assertThat(cm.getData()) + .containsExactlyInAnyOrderEntriesOf(Map.of("key1", "value1", "key2", "value2")); + }); + + deleteInOwnNamespace(glue); + await().pollDelay(INITIAL_RECONCILE_WAIT_TIMEOUT).untilAsserted(() -> { + var g = get(Glue.class, "secret-to-configmap", nsa); + assertThat(g).isNull(); + }); + } + + private Secret createSecretToCopy(String nsb) { + var secret = new Secret(); + secret.setMetadata(new ObjectMetaBuilder() + .withName("secret-to-copy") + .withNamespace(nsb) + .build()); + secret.setData(Map.of("key1", + Base64.getEncoder().encodeToString("value1".getBytes(StandardCharsets.UTF_8)), + "key2", + Base64.getEncoder().encodeToString("value2".getBytes(StandardCharsets.UTF_8)))); + + return client.resource(secret).createOr(NonDeletingOperation::update); + } private List testWorkflowList(int num) { List res = new ArrayList<>(); @@ -423,7 +465,4 @@ private List testWorkflowList(int num) { }); return res; } - - - } diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java b/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java index bdc69d4..afc6499 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java @@ -37,7 +37,7 @@ public void prepareNamespace(TestInfo testInfo) { testInfo.getTestMethod() .ifPresent(method -> testNamespace = KubernetesResourceUtil.sanitizeName(method.getName())); - client.namespaces().resource(testNamespace(testNamespace)).create(); + createNamespace(testNamespace); } @AfterEach @@ -49,7 +49,11 @@ void cleanupNamespace() { }); } - protected Namespace testNamespace(String name) { + protected Namespace createNamespace(String name) { + return client.namespaces().resource(namespace(name)).createOr(NonDeletingOperation::update); + } + + protected Namespace namespace(String name) { return new NamespaceBuilder().withMetadata(new ObjectMetaBuilder() .withName(name) .build()).build(); @@ -72,6 +76,10 @@ protected T get(Class clazz, String name) { return client.resources(clazz).inNamespace(testNamespace).withName(name).get(); } + protected T get(Class clazz, String name, String namespace) { + return client.resources(clazz).inNamespace(namespace).withName(name).get(); + } + protected List list(Class clazz) { return client.resources(clazz).inNamespace(testNamespace).list().getItems(); } @@ -93,6 +101,8 @@ protected void delete(HasMetadata resource) { client.resource(resource).inNamespace(testNamespace).delete(); } - + protected void deleteInOwnNamespace(HasMetadata resource) { + client.resource(resource).delete(); + } } diff --git a/src/test/resources/glue/CopySecretToConfigMap.yaml b/src/test/resources/glue/CopySecretToConfigMap.yaml new file mode 100644 index 0000000..acb98a1 --- /dev/null +++ b/src/test/resources/glue/CopySecretToConfigMap.yaml @@ -0,0 +1,24 @@ +apiVersion: io.javaoperatorsdk.operator.glue/v1beta1 +kind: Glue +metadata: + name: "secret-to-configmap" + namespace: namespace-a +spec: + childResources: + - name: configmap + resourceTemplate: | + apiVersion: v1 + kind: ConfigMap + metadata: + name: my-secret-copy + namespace: namespace-a + data: + {#for entry in secret-to-copy.data} + {entry.key}: {entry.value.decodeBase64} + {/for} + relatedResources: + - name: secret-to-copy + apiVersion: v1 + kind: Secret + resourceNames: ["secret-to-copy"] + namespace: namespace-b