diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/Utils.java b/src/main/java/io/javaoperatorsdk/operator/glue/Utils.java index 7149143..5311c49 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/Utils.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/Utils.java @@ -138,9 +138,7 @@ public static String getKindFromTemplate(String resourceTemplate) { public static Set leafResourceNames(Glue glue) { Set result = new HashSet<>(); glue.getSpec().getChildResources().forEach(r -> result.add(r.getName())); - glue.getSpec().getChildResources().forEach(r -> { - r.getDependsOn().forEach(result::remove); - }); + glue.getSpec().getChildResources().forEach(r -> r.getDependsOn().forEach(result::remove)); return result; } @@ -148,7 +146,13 @@ private static Optional getOptionalPropertyValueFromTemplate(String reso String property) { var finalProp = property + ":"; var targetLine = resourceTemplate.lines().filter(l -> l.contains(finalProp)).findFirst(); - return targetLine.map(l -> l.replace(finalProp, "").trim()); + return targetLine.map(l -> { + int index = l.indexOf(finalProp); + if (index > 0) { + l = l.substring(index); + } + return l.replace(finalProp, "").trim(); + }); } private static String getPropertyValueFromTemplate(String resourceTemplate, String property) { @@ -157,4 +161,10 @@ private static String getPropertyValueFromTemplate(String resourceTemplate, Stri "Template does not contain property. " + resourceTemplate)); } + public static GroupVersionKind getGVKFromTemplate(String resourceTemplate) { + String apiVersion = getApiVersionFromTemplate(resourceTemplate); + String kind = getKindFromTemplate(resourceTemplate); + return new GroupVersionKind(apiVersion, kind); + } + } diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/DependentResourceSpec.java b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/DependentResourceSpec.java index ba929f5..baa7b85 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/DependentResourceSpec.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/customresource/glue/DependentResourceSpec.java @@ -23,6 +23,8 @@ public class DependentResourceSpec { private Matcher matcher = Matcher.SSA; + private Boolean bulk = false; + private List dependsOn = new ArrayList<>(); @PreserveUnknownFields @@ -102,6 +104,14 @@ public void setMatcher(Matcher matcher) { this.matcher = matcher; } + public Boolean getBulk() { + return bulk; + } + + public void setBulk(Boolean bulk) { + this.bulk = bulk; + } + @Override public boolean equals(Object o) { if (this == o) @@ -112,14 +122,14 @@ public boolean equals(Object o) { return clusterScoped == that.clusterScoped && Objects.equals(name, that.name) && Objects.equals(resource, that.resource) && Objects.equals(resourceTemplate, that.resourceTemplate) && matcher == that.matcher - && Objects.equals(dependsOn, that.dependsOn) + && Objects.equals(bulk, that.bulk) && Objects.equals(dependsOn, that.dependsOn) && Objects.equals(readyPostCondition, that.readyPostCondition) && Objects.equals(condition, that.condition); } @Override public int hashCode() { - return Objects.hash(name, clusterScoped, resource, resourceTemplate, matcher, dependsOn, + return Objects.hash(name, clusterScoped, resource, resourceTemplate, matcher, bulk, dependsOn, readyPostCondition, condition); } } diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GCGenericBulkDependentResource.java b/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GCGenericBulkDependentResource.java new file mode 100644 index 0000000..9a735c2 --- /dev/null +++ b/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GCGenericBulkDependentResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.glue.dependent; + +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; +import io.javaoperatorsdk.operator.glue.customresource.glue.Glue; +import io.javaoperatorsdk.operator.glue.customresource.glue.Matcher; +import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler; + +public class GCGenericBulkDependentResource extends GenericBulkDependentResource + implements GarbageCollected { + + public GCGenericBulkDependentResource(GenericTemplateHandler genericTemplateHandler, + String desiredTemplate, String name, + boolean clusterScoped, Matcher matcher) { + super(genericTemplateHandler, desiredTemplate, name, clusterScoped, matcher); + } + +} diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GenericBulkDependentResource.java b/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GenericBulkDependentResource.java new file mode 100644 index 0000000..b509eea --- /dev/null +++ b/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GenericBulkDependentResource.java @@ -0,0 +1,52 @@ +package io.javaoperatorsdk.operator.glue.dependent; + +import java.util.Map; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.glue.customresource.glue.Glue; +import io.javaoperatorsdk.operator.glue.customresource.glue.Matcher; +import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler; +import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource; + +import static io.javaoperatorsdk.operator.glue.reconciler.glue.GlueReconciler.DEPENDENT_NAME_ANNOTATION_KEY; + +public class GenericBulkDependentResource extends + GenericDependentResource implements + BulkDependentResource { + + public GenericBulkDependentResource(GenericTemplateHandler genericTemplateHandler, + String desiredTemplate, String name, + boolean clusterScoped, + Matcher matcher) { + super(genericTemplateHandler, desiredTemplate, name, clusterScoped, matcher); + } + + @Override + public Map desiredResources(Glue primary, + Context context) { + + var res = genericTemplateHandler.processTemplate(desiredTemplate, primary, context); + var desiredList = Serialization.unmarshal(res, GenericKubernetesResourceList.class).getItems(); + desiredList.forEach(r -> { + r.getMetadata().getAnnotations() + .put(DEPENDENT_NAME_ANNOTATION_KEY, name); + if (r.getMetadata().getNamespace() == null && !clusterScoped) { + r.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + } + }); + return desiredList.stream().collect(Collectors.toMap(r -> r.getMetadata().getName(), r -> r)); + } + + @Override + public Map getSecondaryResources(Glue glue, + Context context) { + return context.getSecondaryResources(GenericKubernetesResource.class).stream() + .filter( + r -> name.equals(r.getMetadata().getAnnotations().get(DEPENDENT_NAME_ANNOTATION_KEY))) + .collect(Collectors.toMap(r -> r.getMetadata().getName(), r -> r)); + } +} diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GenericDependentResource.java b/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GenericDependentResource.java index 679f52e..9a2b251 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GenericDependentResource.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/dependent/GenericDependentResource.java @@ -20,14 +20,13 @@ public class GenericDependentResource Updater, Creator { - private final GenericKubernetesResource desired; - private final String desiredTemplate; - private final String name; - private final boolean clusterScoped; - private final Matcher matcher; + protected final GenericKubernetesResource desired; + protected final String desiredTemplate; + protected final String name; + protected final boolean clusterScoped; + protected final Matcher matcher; - // optimize share between instances - private final GenericTemplateHandler genericTemplateHandler; + protected final GenericTemplateHandler genericTemplateHandler; public GenericDependentResource(GenericTemplateHandler genericTemplateHandler, GenericKubernetesResource desired, String name, diff --git a/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/ValidationAndErrorHandler.java b/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/ValidationAndErrorHandler.java index 87f6749..6210329 100644 --- a/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/ValidationAndErrorHandler.java +++ b/src/main/java/io/javaoperatorsdk/operator/glue/reconciler/ValidationAndErrorHandler.java @@ -42,7 +42,20 @@ public class ValidationAndErrorHandler { } } - public void checkIfNamesAreUnique(GlueSpec glueSpec) { + public void checkIfValidGlueSpec(GlueSpec glueSpec) { + checkIfBulkProvidesResourceTemplate(glueSpec); + checkIfNamesAreUnique(glueSpec); + } + + private void checkIfBulkProvidesResourceTemplate(GlueSpec glueSpec) { + glueSpec.getChildResources().forEach(r -> { + if (Boolean.TRUE.equals(r.getBulk()) && r.getResourceTemplate() == null) { + throw new GlueException("Bulk resource requires a template to be set"); + } + }); + } + + void checkIfNamesAreUnique(GlueSpec glueSpec) { Set seen = new HashSet<>(); List duplicates = new ArrayList<>(); 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 9a9571e..34b72d8 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 @@ -21,12 +21,14 @@ import io.javaoperatorsdk.operator.glue.customresource.glue.condition.ConditionSpec; import io.javaoperatorsdk.operator.glue.customresource.glue.condition.JavaScriptConditionSpec; import io.javaoperatorsdk.operator.glue.customresource.glue.condition.ReadyConditionSpec; +import io.javaoperatorsdk.operator.glue.dependent.GCGenericBulkDependentResource; import io.javaoperatorsdk.operator.glue.dependent.GCGenericDependentResource; import io.javaoperatorsdk.operator.glue.dependent.GenericDependentResource; import io.javaoperatorsdk.operator.glue.dependent.GenericResourceDiscriminator; import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler; import io.javaoperatorsdk.operator.glue.reconciler.operator.GlueOperatorReconciler; import io.javaoperatorsdk.operator.glue.templating.GenericTemplateHandler; +import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource; import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; import io.javaoperatorsdk.operator.processing.dependent.workflow.KubernetesResourceDeletedCondition; import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowBuilder; @@ -78,7 +80,7 @@ public UpdateControl reconcile(Glue primary, log.debug("Reconciling glue. name: {} namespace: {}", primary.getMetadata().getName(), primary.getMetadata().getNamespace()); - validationAndErrorHandler.checkIfNamesAreUnique(primary.getSpec()); + validationAndErrorHandler.checkIfValidGlueSpec(primary.getSpec()); registerRelatedResourceInformers(context, primary); if (deletedGlueIfParentMarkedForDeletion(context, primary)) { @@ -183,9 +185,11 @@ private void createAndAddDependentToWorkflow(Glue primary, Context context var dr = createDependentResource(spec, leafDependent, resourceInSameNamespaceAsPrimary); var gvk = dr.getGroupVersionKind(); - dr.setResourceDiscriminator(new GenericResourceDiscriminator(dr.getGroupVersionKind(), - genericTemplateHandler.processTemplate(Utils.getName(spec), primary, context), - targetNamespace.orElse(null))); + if (!(dr instanceof BulkDependentResource)) { + dr.setResourceDiscriminator(new GenericResourceDiscriminator(dr.getGroupVersionKind(), + genericTemplateHandler.processTemplate(Utils.getName(spec), primary, context), + targetNamespace.orElse(null))); + } var es = informerRegister.registerInformer(context, gvk, primary); dr.configureWith(es); @@ -209,9 +213,14 @@ private GenericDependentResource createDependentResource(DependentResourceSpec s if (leafDependent && resourceInSameNamespaceAsPrimary && !spec.isClusterScoped()) { return spec.getResourceTemplate() != null - ? new GCGenericDependentResource(genericTemplateHandler, spec.getResourceTemplate(), - spec.getName(), - spec.isClusterScoped(), spec.getMatcher()) + ? spec.getBulk() + ? new GCGenericBulkDependentResource(genericTemplateHandler, + spec.getResourceTemplate(), + spec.getName(), + spec.isClusterScoped(), spec.getMatcher()) + : new GCGenericDependentResource(genericTemplateHandler, spec.getResourceTemplate(), + spec.getName(), + spec.isClusterScoped(), spec.getMatcher()) : new GCGenericDependentResource(genericTemplateHandler, spec.getResource(), spec.getName(), spec.isClusterScoped(), spec.getMatcher()); 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 6c18137..fb17073 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 @@ -75,7 +75,7 @@ public UpdateControl reconcile(GlueOperator glueOperator, log.info("Reconciling GlueOperator {} in namespace: {}", glueOperator.getMetadata().getName(), glueOperator.getMetadata().getNamespace()); - validationAndErrorHandler.checkIfNamesAreUnique(glueOperator.getSpec()); + validationAndErrorHandler.checkIfValidGlueSpec(glueOperator.getSpec()); var targetCREventSource = getOrRegisterCustomResourceEventSource(glueOperator, context); targetCREventSource.list().forEach(cr -> { diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/GlueOperatorTest.java b/src/test/java/io/javaoperatorsdk/operator/glue/GlueOperatorTest.java index 9ae7409..e1febf8 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/GlueOperatorTest.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/GlueOperatorTest.java @@ -19,6 +19,7 @@ import io.javaoperatorsdk.operator.glue.customresource.operator.GlueOperatorSpec; import io.javaoperatorsdk.operator.glue.customresource.operator.Parent; import io.javaoperatorsdk.operator.glue.reconciler.ValidationAndErrorHandler; +import io.javaoperatorsdk.operator.glue.reconciler.operator.GlueOperatorReconciler; import io.quarkus.test.junit.QuarkusTest; import static io.javaoperatorsdk.operator.glue.TestData.*; @@ -229,6 +230,41 @@ void secretCopySample() { }); } + @Test + void operatorWithBulkResource() { + var go = create(TestUtils + .loadGlueOperator("/glueoperator/BulkOperator.yaml")); + + var cr = testCustomResource(); + cr.getSpec().setReplicas(2); + var createdCR = create(cr); + assertConfigMapsCreated(cr, 2); + + createdCR.getSpec().setReplicas(3); + createdCR = update(createdCR); + assertConfigMapsCreated(cr, 3); + + createdCR.getSpec().setReplicas(1); + createdCR = update(createdCR); + assertConfigMapsCreated(cr, 1); + + delete(createdCR); + assertConfigMapsCreated(cr, 0); + await().untilAsserted(() -> { + var actualCR = get(TestCustomResource.class, cr.getMetadata().getName()); + assertThat(actualCR).isNull(); + }); + + delete(go); + } + + private void assertConfigMapsCreated(TestCustomResource cr, int expected) { + await().untilAsserted(() -> { + var configMaps = getRelatedList(ConfigMap.class, + GlueOperatorReconciler.glueName(cr.getMetadata().getName(), cr.getKind())); + assertThat(configMaps).hasSize(expected); + }); + } GlueOperator testWorkflowOperator() { var wo = new GlueOperator(); diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java index 4ca0f66..cfe62b2 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/GlueTest.java @@ -368,6 +368,25 @@ void customizeMatcher() { }); } + @Test + void simpleBulk() { + var glue = createGlue("/glue/" + "SimpleBulk.yaml"); + + await().untilAsserted(() -> { + var configMaps = getRelatedList(ConfigMap.class, glue.getMetadata().getName()); + assertThat(configMaps).hasSize(3); + assertThat(configMaps) + .allMatch(cm -> cm.getMetadata().getName().startsWith("simple-glue-configmap-")); + }); + + delete(glue); + + await().untilAsserted( + () -> assertThat(getRelatedList(ConfigMap.class, glue.getMetadata().getName()).isEmpty())); + } + + + 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 06b02de..67228e8 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/TestBase.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.glue; import java.time.Duration; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -70,6 +71,18 @@ protected T get(Class clazz, String name) { return client.resources(clazz).inNamespace(testNamespace).withName(name).get(); } + protected List list(Class clazz) { + return client.resources(clazz).inNamespace(testNamespace).list().getItems(); + } + + protected List getRelatedList(Class clazz, String ownerName) { + return list(clazz).stream() + .filter(cm -> !cm.getMetadata().getOwnerReferences().isEmpty() + && cm.getMetadata().getOwnerReferences() + .get(0).getName().equals(ownerName)) + .toList(); + } + protected T update(T resource) { resource.getMetadata().setResourceVersion(null); return client.resource(resource).inNamespace(testNamespace).update(); diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/UtilsTest.java b/src/test/java/io/javaoperatorsdk/operator/glue/UtilsTest.java new file mode 100644 index 0000000..f789f3c --- /dev/null +++ b/src/test/java/io/javaoperatorsdk/operator/glue/UtilsTest.java @@ -0,0 +1,39 @@ +package io.javaoperatorsdk.operator.glue; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class UtilsTest { + + + @Test + void getAPIVersionFromListResource() { + String apiVersion = Utils.getApiVersionFromTemplate(""" + - apiVersion: v1 + kind: ConfigMap + metadata: + name: simple-glue-configmap-{i} + data: + key: "value1" + """); + + assertThat(apiVersion).isEqualTo("v1"); + } + + @Test + void getAPIVersionPFromResource() { + String apiVersion = Utils.getApiVersionFromTemplate(""" + apiVersion: v1 + kind: ConfigMap + metadata: + name: simple-glue-configmap-{i} + data: + key: "value1" + """); + + assertThat(apiVersion).isEqualTo("v1"); + } + +} diff --git a/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResourceSpec.java b/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResourceSpec.java index 40c8768..c6de425 100644 --- a/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResourceSpec.java +++ b/src/test/java/io/javaoperatorsdk/operator/glue/customresource/TestCustomResourceSpec.java @@ -6,6 +6,8 @@ public class TestCustomResourceSpec { private String value; + private Integer replicas; + private List listValues; public String getValue() { @@ -25,4 +27,13 @@ public TestCustomResourceSpec setListValues(List listValues) { this.listValues = listValues; return this; } + + + public Integer getReplicas() { + return replicas; + } + + public void setReplicas(Integer replicas) { + this.replicas = replicas; + } } diff --git a/src/test/resources/glue/SimpleBulk.yaml b/src/test/resources/glue/SimpleBulk.yaml new file mode 100644 index 0000000..d8eeb39 --- /dev/null +++ b/src/test/resources/glue/SimpleBulk.yaml @@ -0,0 +1,19 @@ +# Invalid GLUE, presents resources with non-unique name +apiVersion: io.javaoperatorsdk.operator.glue/v1beta1 +kind: Glue +metadata: + name: simple-glue +spec: + childResources: + - name: configMaps + bulk: true + resourceTemplate: | + items: + {#for i in 3} + - apiVersion: v1 + kind: ConfigMap + metadata: + name: simple-glue-configmap-{i} + data: + key: "value1" + {/for} diff --git a/src/test/resources/glueoperator/BulkOperator.yaml b/src/test/resources/glueoperator/BulkOperator.yaml new file mode 100644 index 0000000..559f8e8 --- /dev/null +++ b/src/test/resources/glueoperator/BulkOperator.yaml @@ -0,0 +1,21 @@ +apiVersion: io.javaoperatorsdk.operator.glue/v1beta1 +kind: GlueOperator +metadata: + name: templating-sample +spec: + parent: + apiVersion: io.javaoperatorsdk.operator.glue/v1 + kind: TestCustomResource + childResources: + - name: configMap1 + bulk: true + resourceTemplate: | + items: + {#for i in parent.spec.replicas} + - apiVersion: v1 + kind: ConfigMap + metadata: + name: {parent.metadata.name}-{i} + data: + key: "value{i}" + {/for} \ No newline at end of file