From 314014a40328c11f16baecb3aa8e1686a2c817b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 29 Dec 2024 16:40:39 +0100 Subject: [PATCH 1/8] feat: status updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../glue/RelatedResourceSpec.java | 30 +++++++++++++--- .../glue/reconciler/glue/GlueReconciler.java | 35 ++++++++++++++++++- .../templating/GenericTemplateHandler.java | 23 +++++++++--- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/RelatedResourceSpec.java b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/RelatedResourceSpec.java index 8b04101..ae8c208 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/RelatedResourceSpec.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/RelatedResourceSpec.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Objects; +import io.fabric8.crd.generator.annotation.PreserveUnknownFields; import io.fabric8.generator.annotation.Required; public class RelatedResourceSpec { @@ -19,6 +20,9 @@ public class RelatedResourceSpec { private boolean clusterScoped = Boolean.FALSE; private List resourceNames; + @PreserveUnknownFields + private Object statusPatch; + private String statusPatchTemplate; public String getApiVersion() { return apiVersion; @@ -73,14 +77,16 @@ public boolean equals(Object o) { return false; RelatedResourceSpec that = (RelatedResourceSpec) o; return clusterScoped == that.clusterScoped && Objects.equals(name, that.name) - && Objects.equals(apiVersion, that.apiVersion) && Objects.equals(kind, that.kind) - && Objects.equals(namespace, that.namespace) - && Objects.equals(resourceNames, that.resourceNames); + && Objects.equals(namespace, that.namespace) && Objects.equals(apiVersion, that.apiVersion) + && Objects.equals(kind, that.kind) && Objects.equals(resourceNames, that.resourceNames) + && Objects.equals(statusPatch, that.statusPatch) + && Objects.equals(statusPatchTemplate, that.statusPatchTemplate); } @Override public int hashCode() { - return Objects.hash(name, apiVersion, kind, clusterScoped, namespace, resourceNames); + return Objects.hash(name, namespace, apiVersion, kind, clusterScoped, resourceNames, + statusPatch, statusPatchTemplate); } public boolean isClusterScoped() { @@ -90,4 +96,20 @@ public boolean isClusterScoped() { public void setClusterScoped(boolean clusterScoped) { this.clusterScoped = clusterScoped; } + + public Object getStatusPatch() { + return statusPatch; + } + + public void setStatusPatch(Object statusPatch) { + this.statusPatch = statusPatch; + } + + public String getStatusPatchTemplate() { + return statusPatchTemplate; + } + + public void setStatusPatchTemplate(String statusPatchTemplate) { + this.statusPatchTemplate = statusPatchTemplate; + } } diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java b/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java index 3e88d67..245cbdb 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java @@ -10,6 +10,7 @@ import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.base.PatchContext; import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.glue.Utils; import io.javaoperatorsdk.operator.glue.conditions.JavaScripCondition; @@ -47,6 +48,7 @@ public class GlueReconciler implements Reconciler, Cleaner, ErrorSta private final ValidationAndErrorHandler validationAndErrorHandler; private final InformerRegister informerRegister; + private final GenericTemplateHandler templateHandler; private final KubernetesResourceDeletedCondition deletePostCondition = new KubernetesResourceDeletedCondition(); @@ -54,10 +56,11 @@ public class GlueReconciler implements Reconciler, Cleaner, ErrorSta private final GenericTemplateHandler genericTemplateHandler; public GlueReconciler(ValidationAndErrorHandler validationAndErrorHandler, - InformerRegister informerRegister, + InformerRegister informerRegister, GenericTemplateHandler templateHandler, GenericTemplateHandler genericTemplateHandler) { this.validationAndErrorHandler = validationAndErrorHandler; this.informerRegister = informerRegister; + this.templateHandler = templateHandler; this.genericTemplateHandler = genericTemplateHandler; } @@ -89,6 +92,7 @@ public UpdateControl reconcile(Glue primary, cleanupRemovedResourcesFromWorkflow(context, primary); informerRegister.deRegisterInformerOnResourceFlowChange(context, primary); result.throwAggregateExceptionIfErrorsPresent(); + patchRelatedResourcesStatus(context, primary); return UpdateControl.noUpdate(); } @@ -222,6 +226,35 @@ private GenericDependentResource createDependentResource(DependentResourceSpec s } } + // todo add workflow result? + private void patchRelatedResourcesStatus(Context context, + Glue primary) { + + var targetRelatedResources = primary.getSpec().getRelatedResources().stream() + .filter(r -> r.getStatusPatch() != null || r.getStatusPatchTemplate() != null) + .toList(); + + if (!targetRelatedResources.isEmpty()) { + return; + } + var actualData = genericTemplateHandler.createDataWithResources(primary, context); + + targetRelatedResources.forEach(r -> { + var relatedResources = Utils.getRelatedResources(primary, r, context); + + var template = r.getStatusPatchTemplate() != null ? r.getStatusPatchTemplate() + : Serialization.asYaml(r.getStatusPatch()); + var resultTemplate = + genericTemplateHandler.processTemplate(actualData, template); + var statusObjectMap = GenericTemplateHandler.parseTemplateToMapObject(resultTemplate); + relatedResources.forEach((n, kr) -> { + kr.setAdditionalProperty("status", statusObjectMap); + context.getClient().resource(kr).patchStatus(); + }); + }); + + } + @SuppressWarnings({"rawtypes"}) private Condition toCondition(ConditionSpec condition) { if (condition instanceof ReadyConditionSpec readyConditionSpec) { 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 b7c77cc..f6325c7 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java @@ -29,20 +29,26 @@ public String processTemplate(Map> data, String template) { public String processInputAndTemplate(Map data, String template) { - Map> res = new HashMap<>(); - data.forEach((key, value) -> res.put(key, - value == null ? null : objectMapper.convertValue(value, Map.class))); + Map> res = + genericKubernetesResourceDataToGenericData(data); return processTemplate(res, template); } public String processTemplate(String template, Glue primary, Context context) { - var data = createDataWithResources(primary, context); return processTemplate(data, template); } - private static Map> createDataWithResources(Glue primary, + private static Map> genericKubernetesResourceDataToGenericData( + Map data) { + Map> res = new HashMap<>(); + data.forEach((key, value) -> res.put(key, + value == null ? null : objectMapper.convertValue(value, Map.class))); + return res; + } + + public static Map> createDataWithResources(Glue primary, Context context) { Map> res = new HashMap<>(); var actualResourcesByName = Utils.getActualResourcesByNameInWorkflow(context, primary); @@ -55,4 +61,11 @@ public String processTemplate(String template, Glue primary, return res; } + + // todo unit test + @SuppressWarnings("unchecked") + public static Map parseTemplateToMapObject(String template) { + return objectMapper.convertValue(template, Map.class); + } + } From 4f9e6a5e959a5f69a767a483473646253bc7df1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 29 Dec 2024 16:59:07 +0100 Subject: [PATCH 2/8] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../glue/customresource/TestCustomResource.java | 3 ++- .../TestCustomResourceStatus.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResourceStatus.java diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResource.java b/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResource.java index bb661c2..fe72216 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResource.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResource.java @@ -9,7 +9,8 @@ @Group(TestCustomResource.CR_GROUP) @Version("v1") @ShortNames("tcr") -public class TestCustomResource extends CustomResource +public class TestCustomResource + extends CustomResource implements Namespaced { public static final String CR_GROUP = "io.javaoperatorsdk.operator.glue"; diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResourceStatus.java b/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResourceStatus.java new file mode 100644 index 0000000..28ecc02 --- /dev/null +++ b/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResourceStatus.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.glue.customresource; + + + +public class TestCustomResourceStatus { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} From 27dbbebc1046eba0ab534d6d8dab8233a0dc4284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 29 Dec 2024 18:27:47 +0100 Subject: [PATCH 3/8] IT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../glue/reconciler/glue/GlueReconciler.java | 6 ++-- .../templating/GenericTemplateHandler.java | 4 +-- .../operator/glue/GlueTest.java | 28 +++++++++++++++++++ .../operator/glue/TestBase.java | 6 ++++ .../resources/glue/PatchRelatedStatus.yaml | 21 ++++++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/glue/PatchRelatedStatus.yaml diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java b/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java index 245cbdb..fd292af 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/glue/GlueReconciler.java @@ -48,7 +48,6 @@ public class GlueReconciler implements Reconciler, Cleaner, ErrorSta private final ValidationAndErrorHandler validationAndErrorHandler; private final InformerRegister informerRegister; - private final GenericTemplateHandler templateHandler; private final KubernetesResourceDeletedCondition deletePostCondition = new KubernetesResourceDeletedCondition(); @@ -56,11 +55,10 @@ public class GlueReconciler implements Reconciler, Cleaner, ErrorSta private final GenericTemplateHandler genericTemplateHandler; public GlueReconciler(ValidationAndErrorHandler validationAndErrorHandler, - InformerRegister informerRegister, GenericTemplateHandler templateHandler, + InformerRegister informerRegister, GenericTemplateHandler genericTemplateHandler) { this.validationAndErrorHandler = validationAndErrorHandler; this.informerRegister = informerRegister; - this.templateHandler = templateHandler; this.genericTemplateHandler = genericTemplateHandler; } @@ -234,7 +232,7 @@ private void patchRelatedResourcesStatus(Context context, .filter(r -> r.getStatusPatch() != null || r.getStatusPatchTemplate() != null) .toList(); - if (!targetRelatedResources.isEmpty()) { + if (targetRelatedResources.isEmpty()) { return; } var actualData = genericTemplateHandler.createDataWithResources(primary, context); 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 f6325c7..c938aea 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/templating/GenericTemplateHandler.java @@ -4,6 +4,7 @@ import java.util.Map; import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.glue.Utils; import io.javaoperatorsdk.operator.glue.customresource.glue.Glue; @@ -62,10 +63,9 @@ public String processTemplate(String template, Glue primary, return res; } - // todo unit test @SuppressWarnings("unchecked") public static Map parseTemplateToMapObject(String template) { - return objectMapper.convertValue(template, Map.class); + return Serialization.unmarshal(template, Map.class); } } diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java index 8c5e123..036f99f 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java @@ -14,6 +14,7 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; +import io.javaoperatorsdk.operator.glue.customresource.TestCustomResource; import io.javaoperatorsdk.operator.glue.customresource.glue.DependentResourceSpec; import io.javaoperatorsdk.operator.glue.customresource.glue.Glue; import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler; @@ -324,6 +325,33 @@ void clusterScopedRelatedResource() { }); } + @Test + void pathRelatedResourceStatus() { + TestUtils.applyTestCrd(client, TestCustomResource.class); + + var customResource = create(TestData.testCustomResource()); + var glue = createGlue("/glue/PatchRelatedStatus.yaml"); + + await().untilAsserted(() -> { + var cm = get(ConfigMap.class, "configmap1"); + assertThat(cm).isNotNull(); + var cr = get(TestCustomResource.class, "testcr1"); + assertThat(cr.getStatus()).isNotNull(); + assertThat(cr.getStatus().getValue()).isEqualTo(cm.getMetadata().getResourceVersion()); + }); + + delete(glue); + + await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> { + var cm = get(ConfigMap.class, "configmap1"); + assertThat(cm).isNull(); + }); + + delete(customResource); + } + + + private List testWorkflowList(int num) { List res = new ArrayList<>(); IntStream.range(0, num).forEach(index -> { diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java b/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java index db9b10f..06b02de 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java @@ -13,9 +13,11 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.javaoperatorsdk.operator.glue.customresource.glue.Glue; import jakarta.inject.Inject; +import static io.javaoperatorsdk.operator.glue.TestUtils.loadGlue; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -51,6 +53,10 @@ protected Namespace testNamespace(String name) { .build()).build(); } + protected Glue createGlue(String path) { + return create(loadGlue(path)); + } + protected T create(T resource) { return client.resource(resource).inNamespace(testNamespace).create(); } diff --git a/src/test/resources/glue/PatchRelatedStatus.yaml b/src/test/resources/glue/PatchRelatedStatus.yaml new file mode 100644 index 0000000..c40cb6d --- /dev/null +++ b/src/test/resources/glue/PatchRelatedStatus.yaml @@ -0,0 +1,21 @@ +apiVersion: io.javaoperatorsdk.operator.glue/v1beta1 +kind: Glue +metadata: + name: related-resource-status-patch +spec: + childResources: + - name: configMap1 + resource: + apiVersion: v1 + kind: ConfigMap + metadata: + name: configmap1 + data: + key: "value" + relatedResources: + - name: related + apiVersion: "io.javaoperatorsdk.operator.glue/v1" + kind: TestCustomResource + resourceNames: ["testcr1"] + statusPatch: + value: "{configMap1.metadata.resourceVersion}" From feaa239466b158ea9a51b9769aebb95b0beee90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 29 Dec 2024 18:34:03 +0100 Subject: [PATCH 4/8] test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/glue/GlueTest.java | 18 +++++++++------- .../glue/PatchRelatedStatusWithTemplate.yaml | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/glue/PatchRelatedStatusWithTemplate.yaml diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java index 036f99f..21efbd6 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java @@ -19,6 +19,8 @@ import io.javaoperatorsdk.operator.glue.customresource.glue.Glue; import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler; import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -325,12 +327,13 @@ void clusterScopedRelatedResource() { }); } - @Test - void pathRelatedResourceStatus() { + @ParameterizedTest + @ValueSource(strings = {"PatchRelatedStatus.yaml","PatchRelatedStatusWithTemplate.yaml"}) + void pathRelatedResourceStatus(String glueFileName) { TestUtils.applyTestCrd(client, TestCustomResource.class); var customResource = create(TestData.testCustomResource()); - var glue = createGlue("/glue/PatchRelatedStatus.yaml"); + var glue = createGlue("/glue/"+glueFileName); await().untilAsserted(() -> { var cm = get(ConfigMap.class, "configmap1"); @@ -339,19 +342,18 @@ void pathRelatedResourceStatus() { assertThat(cr.getStatus()).isNotNull(); assertThat(cr.getStatus().getValue()).isEqualTo(cm.getMetadata().getResourceVersion()); }); - delete(glue); - await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> { var cm = get(ConfigMap.class, "configmap1"); assertThat(cm).isNull(); }); - delete(customResource); + await().untilAsserted(() -> { + var cr = get(TestCustomResource.class, "testcr1"); + assertThat(cr).isNull(); + }); } - - private List testWorkflowList(int num) { List res = new ArrayList<>(); IntStream.range(0, num).forEach(index -> { diff --git a/src/test/resources/glue/PatchRelatedStatusWithTemplate.yaml b/src/test/resources/glue/PatchRelatedStatusWithTemplate.yaml new file mode 100644 index 0000000..ac4c7e8 --- /dev/null +++ b/src/test/resources/glue/PatchRelatedStatusWithTemplate.yaml @@ -0,0 +1,21 @@ +apiVersion: io.javaoperatorsdk.operator.glue/v1beta1 +kind: Glue +metadata: + name: related-resource-status-patch +spec: + childResources: + - name: configMap1 + resource: + apiVersion: v1 + kind: ConfigMap + metadata: + name: configmap1 + data: + key: "value" + relatedResources: + - name: related + apiVersion: "io.javaoperatorsdk.operator.glue/v1" + kind: TestCustomResource + resourceNames: ["testcr1"] + statusPatchTemplate: | + value: "{configMap1.metadata.resourceVersion}" From 4ca892c2791f170c1d9124f424cf5f0fb469ed0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 29 Dec 2024 18:49:56 +0100 Subject: [PATCH 5/8] format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../glue/customresource/operator/GlueOperatorSpec.java | 1 + .../java/io/javaoperatorsdk/operator/glue/GlueTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/GlueOperatorSpec.java b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/GlueOperatorSpec.java index 3fd4e06..c5b6e83 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/GlueOperatorSpec.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/GlueOperatorSpec.java @@ -12,6 +12,7 @@ public class GlueOperatorSpec extends GlueSpec { private GlueMetadata glueMetadata; + public Parent getParent() { return parent; } diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java index 21efbd6..808171b 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java @@ -8,6 +8,8 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; @@ -19,8 +21,6 @@ import io.javaoperatorsdk.operator.glue.customresource.glue.Glue; import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler; import io.quarkus.test.junit.QuarkusTest; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -328,12 +328,12 @@ void clusterScopedRelatedResource() { } @ParameterizedTest - @ValueSource(strings = {"PatchRelatedStatus.yaml","PatchRelatedStatusWithTemplate.yaml"}) + @ValueSource(strings = {"PatchRelatedStatus.yaml", "PatchRelatedStatusWithTemplate.yaml"}) void pathRelatedResourceStatus(String glueFileName) { TestUtils.applyTestCrd(client, TestCustomResource.class); var customResource = create(TestData.testCustomResource()); - var glue = createGlue("/glue/"+glueFileName); + var glue = createGlue("/glue/" + glueFileName); await().untilAsserted(() -> { var cm = get(ConfigMap.class, "configmap1"); From 38d3e30f86d38652a78d2536abb3954dc5ce91b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 29 Dec 2024 19:07:56 +0100 Subject: [PATCH 6/8] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/GlueOperatorSpec.java | 1 - .../glue/customresource/operator/Parent.java | 27 +++++++++++++++++-- .../operator/GlueOperatorReconciler.java | 20 ++++++++++---- .../glue/sample/webpage/WebPageE2E.java | 2 ++ .../glue/sample/webpage/WebPageStatus.java | 16 ++++++----- .../sample/webpage/webpage.operator.yaml | 2 ++ 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/GlueOperatorSpec.java b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/GlueOperatorSpec.java index c5b6e83..3fd4e06 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/GlueOperatorSpec.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/GlueOperatorSpec.java @@ -12,7 +12,6 @@ public class GlueOperatorSpec extends GlueSpec { private GlueMetadata glueMetadata; - public Parent getParent() { return parent; } diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/Parent.java b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/Parent.java index b8c110a..1e40b42 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/Parent.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/operator/Parent.java @@ -2,6 +2,8 @@ import java.util.Objects; +import io.fabric8.crd.generator.annotation.PreserveUnknownFields; + public class Parent { private String apiVersion; @@ -9,6 +11,9 @@ public class Parent { private boolean clusterScoped = false; private String labelSelector; + @PreserveUnknownFields + private Object status; + private String statusTemplate; public Parent() {} @@ -51,6 +56,22 @@ public void setClusterScoped(boolean clusterScoped) { this.clusterScoped = clusterScoped; } + public Object getStatus() { + return status; + } + + public void setStatus(Object status) { + this.status = status; + } + + public String getStatusTemplate() { + return statusTemplate; + } + + public void setStatusTemplate(String statusTemplate) { + this.statusTemplate = statusTemplate; + } + @Override public boolean equals(Object o) { if (this == o) @@ -59,11 +80,13 @@ public boolean equals(Object o) { return false; Parent parent = (Parent) o; return clusterScoped == parent.clusterScoped && Objects.equals(apiVersion, parent.apiVersion) - && Objects.equals(kind, parent.kind) && Objects.equals(labelSelector, parent.labelSelector); + && Objects.equals(kind, parent.kind) && Objects.equals(labelSelector, parent.labelSelector) + && Objects.equals(status, parent.status) + && Objects.equals(statusTemplate, parent.statusTemplate); } @Override public int hashCode() { - return Objects.hash(apiVersion, kind, clusterScoped, labelSelector); + return Objects.hash(apiVersion, kind, clusterScoped, labelSelector, status, statusTemplate); } } diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/operator/GlueOperatorReconciler.java b/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/operator/GlueOperatorReconciler.java index 965a91c..6c18137 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/operator/GlueOperatorReconciler.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/operator/GlueOperatorReconciler.java @@ -19,6 +19,7 @@ import io.javaoperatorsdk.operator.glue.customresource.glue.RelatedResourceSpec; import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperator; import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperatorSpec; +import io.javaoperatorsdk.operator.glue.customresource.operator.Parent; import io.javaoperatorsdk.operator.glue.customresource.operator.ResourceFlowOperatorStatus; import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler; import io.javaoperatorsdk.operator.glue.reconciler.glue.GlueReconciler; @@ -103,7 +104,6 @@ private Glue createGlue(GenericKubernetesResource targetParentResource, ObjectMeta glueMetadata = glueMetadata(glueOperator, targetParentResource); - glue.setMetadata(glueMetadata); glue.setSpec(toWorkflowSpec(glueOperator.getSpec())); @@ -112,6 +112,16 @@ private Glue createGlue(GenericKubernetesResource targetParentResource, } var parent = glueOperator.getSpec().getParent(); + RelatedResourceSpec parentRelatedSpec = + parentRelatedResourceSpec(targetParentResource, glueOperator, parent); + + glue.getSpec().getRelatedResources().add(parentRelatedSpec); + glue.addOwnerReference(targetParentResource); + return glue; + } + + private static RelatedResourceSpec parentRelatedResourceSpec( + GenericKubernetesResource targetParentResource, GlueOperator glueOperator, Parent parent) { RelatedResourceSpec parentRelatedSpec = new RelatedResourceSpec(); parentRelatedSpec.setName(PARENT_RELATED_RESOURCE_NAME); parentRelatedSpec.setApiVersion(parent.getApiVersion()); @@ -119,10 +129,10 @@ private Glue createGlue(GenericKubernetesResource targetParentResource, parentRelatedSpec.setResourceNames(List.of(targetParentResource.getMetadata().getName())); parentRelatedSpec.setNamespace(targetParentResource.getMetadata().getNamespace()); parentRelatedSpec.setClusterScoped(glueOperator.getSpec().getParent().isClusterScoped()); - - glue.getSpec().getRelatedResources().add(parentRelatedSpec); - glue.addOwnerReference(targetParentResource); - return glue; + parentRelatedSpec + .setStatusPatchTemplate(glueOperator.getSpec().getParent().getStatusTemplate()); + parentRelatedSpec.setStatusPatch(glueOperator.getSpec().getParent().getStatus()); + return parentRelatedSpec; } private ObjectMeta glueMetadata(GlueOperator glueOperator, diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageE2E.java b/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageE2E.java index 479c7f0..cfbf43e 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageE2E.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageE2E.java @@ -62,6 +62,7 @@ void testWebPageCRUDOperations() { client.resource(createdWebPage).delete(); await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> { + var wp = client.resources(WebPage.class).withName("webpage1").get(); var deployment = client.resources(Deployment.class).withName(webPage.getMetadata().getName()).get(); var configMap = @@ -73,6 +74,7 @@ void testWebPageCRUDOperations() { assertThat(configMap).isNull(); assertThat(service).isNull(); assertThat(ingress).isNull(); + assertThat(wp.getStatus().getObservedGeneration()).isNotNull(); }); } diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageStatus.java b/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageStatus.java index dad7837..30aec42 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageStatus.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageStatus.java @@ -1,11 +1,15 @@ package io.javaoperatorsdk.operator.glue.sample.webpage; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -@JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) -@JsonPropertyOrder({}) -@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class) public class WebPageStatus { + + private Long observedGeneration; + + public Long getObservedGeneration() { + return observedGeneration; + } + + public void setObservedGeneration(Long observedGeneration) { + this.observedGeneration = observedGeneration; + } } diff --git a/src/test/resources/sample/webpage/webpage.operator.yaml b/src/test/resources/sample/webpage/webpage.operator.yaml index 87ef529..6fb251c 100644 --- a/src/test/resources/sample/webpage/webpage.operator.yaml +++ b/src/test/resources/sample/webpage/webpage.operator.yaml @@ -6,6 +6,8 @@ spec: parent: apiVersion: glueoperator.sample/v1 kind: WebPage + status: + observedGeneration: "{parent.metadata.generation}" childResources: - name: htmlconfigmap resource: From 1736095b72f425af04a3e0d9bb9e6c8e6a03891c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 29 Dec 2024 19:32:43 +0100 Subject: [PATCH 7/8] docs & test fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- README.md | 6 +++++- docs/reference.md | 10 ++++++++++ .../operator/glue/sample/webpage/WebPageE2E.java | 6 ++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 402e3fb..62e1076 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ In other words, it also allows you to write **workflows** over resources in a ** ## Contact Us -Either in the discussion section here on GitHub or at [Kubernetes Slack Operator Channel](https://kubernetes.slack.com/archives/CAW0GV7A5). +Either in the discussion section here on GitHub or at [Kubernetes Slack Operator Channel](https://kubernetes.slack.com/archives/CAW0GV7A5). While +in "object" form only placeholder substitutions are possible, in string template you can use all the +features of qute. ## Quick Introduction @@ -61,6 +63,8 @@ spec: parent: apiVersion: glueoperator.sample/v1 # watches all the custom resource of type WebPage kind: WebPage + status: # update the status of the custom resource at the end of reconciliation + observedGeneration: "{parent.metadata.generation}" childResources: - name: htmlconfigmap resource: diff --git a/docs/reference.md b/docs/reference.md index 3f158f2..02aae7e 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -6,6 +6,10 @@ of [Java Operator SDK](https://github.com/operator-framework/java-operator-sdk) Although it is limited only to Kubernetes resources it makes it very easy to use in language-independent (DependentResources in JOSDK are also covering external resources) way. +## Generic Notes + +- All templates (both object and string based) uses [Qute templating engine](https://quarkus.io/guides/qute-reference). + ## [Glue resource](https://github.com/java-operator-sdk/kubernetes-glue-operator/releases/latest/download/glues.glue-v1.yml) `Glue` is the heart of the operator. Note that `GlueOperator` controller just creates a new `Glue` with a related resource, @@ -57,6 +61,9 @@ The following attributes can be defined for a related resource: - **`apiVersion`** - Kubernetes resource API Version of the resource - **`kind`** - Kubernetes kind property of the resource - **`resourceNames`** - list of string of the resource names within the same namespace as `Glue`. +- **`statusPatch`** - template object used to update status of the related resource at the end of the reconciliation. See [sample](https://github.com/java-operator-sdk/kubernetes-glue-operator/blob/main/src/test/resources/glue/PatchRelatedStatus.yaml#L20-L21). + All the available resources (child, related) are provided. +- **`statusPatchTemplate`** - same as `statusPatch` just as a string template. See [sample](https://github.com/java-operator-sdk/kubernetes-glue-operator/blob/main/src/test/resources/glue/PatchRelatedStatusWithTemplate.yaml#L20-L21). ### Referencing other resources @@ -91,6 +98,9 @@ The specs of `GlueOperator` are almost identical to `Glue`, it just adds some ad - **`apiVersion`** and **`kind`** - of the target custom resources. - **`labelSelector`** - optional label selector for the target resources. - **`clusterScoped`** - optional boolean value, if the parent resource is cluster scoped. Default is `false`. + - **`status`** - template object to update status of the related resource at the end of the reconciliation. + All the available resources (parent, child, related) are available. + - **`statusTemplate`** - same as `status` just as a string template. - **`glueMetadata`** - optionally, you can customize the `Glue` resource created for each parent resource. This is especially important when the parent is a cluster scoped resource - in that case it is mandatory to set. Using this you can specify the **`name`** and **`namespace`** of the created `Glue`. diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageE2E.java b/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageE2E.java index cfbf43e..c4763ba 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageE2E.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageE2E.java @@ -59,10 +59,12 @@ void testWebPageCRUDOperations() { assertThat(ingress).isNotNull(); }); + var wp = client.resources(WebPage.class).withName("webpage1").get(); + assertThat(wp.getStatus().getObservedGeneration()).isNotNull(); + client.resource(createdWebPage).delete(); await().timeout(TestUtils.GC_WAIT_TIMEOUT).untilAsserted(() -> { - var wp = client.resources(WebPage.class).withName("webpage1").get(); var deployment = client.resources(Deployment.class).withName(webPage.getMetadata().getName()).get(); var configMap = @@ -74,7 +76,7 @@ void testWebPageCRUDOperations() { assertThat(configMap).isNull(); assertThat(service).isNull(); assertThat(ingress).isNull(); - assertThat(wp.getStatus().getObservedGeneration()).isNotNull(); + }); } From 6886843083ccf264151c569a000d55dcb7fa6f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 29 Dec 2024 19:42:51 +0100 Subject: [PATCH 8/8] fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- README.md | 4 ++-- .../operator/glue/sample/webpage/WebPageSampleTest.java | 3 +++ src/test/resources/sample/webpage/webpage.crd.yaml | 3 +++ src/test/resources/sample/webpage/webpage.operator.yaml | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 62e1076..ff84fdd 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ spec: parent: apiVersion: glueoperator.sample/v1 # watches all the custom resource of type WebPage kind: WebPage - status: # update the status of the custom resource at the end of reconciliation - observedGeneration: "{parent.metadata.generation}" + statusTemplate: | # update the status of the custom resource at the end of reconciliation + observedGeneration: {parent.metadata.generation} childResources: - name: htmlconfigmap resource: diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageSampleTest.java b/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageSampleTest.java index 9eee921..ebe61f9 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageSampleTest.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/sample/webpage/WebPageSampleTest.java @@ -53,9 +53,12 @@ void webPageCRUD() { await().untilAsserted(() -> { var ingress = get(Ingress.class, webPage.getMetadata().getName()); var configMap = get(ConfigMap.class, webPage.getMetadata().getName()); + var wp = get(WebPage.class, webPage.getMetadata().getName()); assertThat(configMap.getData().get("index.html")).contains("Hello World 2!"); assertThat(ingress).isNotNull(); + assertThat(wp.getStatus()).isNotNull(); + assertThat(wp.getStatus().getObservedGeneration()).isNotNull(); }); delete(webPage); diff --git a/src/test/resources/sample/webpage/webpage.crd.yaml b/src/test/resources/sample/webpage/webpage.crd.yaml index a0025f3..8aeca13 100644 --- a/src/test/resources/sample/webpage/webpage.crd.yaml +++ b/src/test/resources/sample/webpage/webpage.crd.yaml @@ -24,6 +24,9 @@ spec: type: object status: type: object + properties: + observedGeneration: + type: integer type: object served: true storage: true diff --git a/src/test/resources/sample/webpage/webpage.operator.yaml b/src/test/resources/sample/webpage/webpage.operator.yaml index 6fb251c..53606fe 100644 --- a/src/test/resources/sample/webpage/webpage.operator.yaml +++ b/src/test/resources/sample/webpage/webpage.operator.yaml @@ -6,8 +6,8 @@ spec: parent: apiVersion: glueoperator.sample/v1 kind: WebPage - status: - observedGeneration: "{parent.metadata.generation}" + statusTemplate: | + observedGeneration: {parent.metadata.generation} childResources: - name: htmlconfigmap resource: