diff --git a/docs/reference.md b/docs/reference.md index 1eada14..bf28179 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -84,12 +84,17 @@ The `DependentResource` implementation of JOSDK makes all kinds of optimizations ## [GlueOperator resource](https://github.com/csviri/kubernetes-glue-operator/releases/latest/download/glueoperators.glue-v1.yml) -The specs of `GlueOperator` are almost identical to `Glue`, it just adds one additional attribute **`parent`**, -which has the following sub-attributes: - - **`apiVersion`** and **`kind`** - specifies the resources to reconciler according to the spec. - Targets are usually custom resources but not necessarily, it also works with built-in Kubernetes - resources. - - **`labelSelector`** - an optional label selector for the target resources +The specs of `GlueOperator` are almost identical to `Glue`, it just adds some additional attributes: + + - **`parent`** - specifies the resources handled by the operator. Targets are usually custom resources but not necessarily, + it also works with built-in Kubernetes resources. With the following sub-attributes: + - **`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`. + - **`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`. + See usage on the sample [secret-copy-operator](https://github.com/csviri/kubernetes-glue-operator/blob/main/src/test/resources/sample/secretcopy/secret-copy.operator.yaml#L10-L12). See minimal `GlueOperator` [here](https://github.com/csviri/kubernetes-glue-operator/blob/main/src/test/resources/glueoperator/SimpleGlueOperator.yaml). diff --git a/src/main/java/io/csviri/operator/glue/customresource/operator/GlueMetadata.java b/src/main/java/io/csviri/operator/glue/customresource/operator/GlueMetadata.java new file mode 100644 index 0000000..011565c --- /dev/null +++ b/src/main/java/io/csviri/operator/glue/customresource/operator/GlueMetadata.java @@ -0,0 +1,40 @@ +package io.csviri.operator.glue.customresource.operator; + +import java.util.Objects; + +public class GlueMetadata { + + private String name; + private String namespace; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + GlueMetadata that = (GlueMetadata) o; + return Objects.equals(name, that.name) && Objects.equals(namespace, that.namespace); + } + + @Override + public int hashCode() { + return Objects.hash(name, namespace); + } +} diff --git a/src/main/java/io/csviri/operator/glue/customresource/operator/GlueOperatorSpec.java b/src/main/java/io/csviri/operator/glue/customresource/operator/GlueOperatorSpec.java index 2cc23dd..f485e63 100644 --- a/src/main/java/io/csviri/operator/glue/customresource/operator/GlueOperatorSpec.java +++ b/src/main/java/io/csviri/operator/glue/customresource/operator/GlueOperatorSpec.java @@ -10,6 +10,8 @@ public class GlueOperatorSpec extends GlueSpec { @Required private Parent parent; + private GlueMetadata glueMetadata; + public Parent getParent() { return parent; } @@ -19,6 +21,14 @@ public GlueOperatorSpec setParent(Parent parent) { return this; } + public GlueMetadata getGlueMetadata() { + return glueMetadata; + } + + public void setGlueMetadata(GlueMetadata glueMetadata) { + this.glueMetadata = glueMetadata; + } + @Override public boolean equals(Object o) { if (this == o) @@ -28,12 +38,11 @@ public boolean equals(Object o) { if (!super.equals(o)) return false; GlueOperatorSpec that = (GlueOperatorSpec) o; - return Objects.equals(parent, that.parent); + return Objects.equals(parent, that.parent) && Objects.equals(glueMetadata, that.glueMetadata); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), parent); + return Objects.hash(super.hashCode(), parent, glueMetadata); } - } diff --git a/src/main/java/io/csviri/operator/glue/customresource/operator/Parent.java b/src/main/java/io/csviri/operator/glue/customresource/operator/Parent.java index 45a6f5e..54fa936 100644 --- a/src/main/java/io/csviri/operator/glue/customresource/operator/Parent.java +++ b/src/main/java/io/csviri/operator/glue/customresource/operator/Parent.java @@ -1,11 +1,15 @@ package io.csviri.operator.glue.customresource.operator; +import java.util.Objects; + public class Parent { private String apiVersion; private String kind; + private boolean clusterScoped = false; private String labelSelector; + public Parent() {} public Parent(String apiVersion, String kind) { @@ -38,4 +42,28 @@ public String getLabelSelector() { public void setLabelSelector(String labelSelector) { this.labelSelector = labelSelector; } + + public boolean isClusterScoped() { + return clusterScoped; + } + + public void setClusterScoped(boolean clusterScoped) { + this.clusterScoped = clusterScoped; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + 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); + } + + @Override + public int hashCode() { + return Objects.hash(apiVersion, kind, clusterScoped, labelSelector); + } } diff --git a/src/main/java/io/csviri/operator/glue/dependent/GCGenericDependentResource.java b/src/main/java/io/csviri/operator/glue/dependent/GCGenericDependentResource.java index 408bb76..021f513 100644 --- a/src/main/java/io/csviri/operator/glue/dependent/GCGenericDependentResource.java +++ b/src/main/java/io/csviri/operator/glue/dependent/GCGenericDependentResource.java @@ -1,18 +1,21 @@ package io.csviri.operator.glue.dependent; import io.csviri.operator.glue.customresource.glue.Glue; +import io.csviri.operator.glue.templating.GenericTemplateHandler; import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; public class GCGenericDependentResource extends GenericDependentResource implements GarbageCollected { - public GCGenericDependentResource(GenericKubernetesResource desired, String name, + public GCGenericDependentResource(GenericTemplateHandler genericTemplateHandler, + GenericKubernetesResource desired, String name, boolean clusterScoped) { - super(desired, name, clusterScoped); + super(genericTemplateHandler, desired, name, clusterScoped); } - public GCGenericDependentResource(String desiredTemplate, String name, boolean clusterScoped) { - super(desiredTemplate, name, clusterScoped); + public GCGenericDependentResource(GenericTemplateHandler genericTemplateHandler, + String desiredTemplate, String name, boolean clusterScoped) { + super(genericTemplateHandler, desiredTemplate, name, clusterScoped); } } diff --git a/src/main/java/io/csviri/operator/glue/dependent/GenericDependentResource.java b/src/main/java/io/csviri/operator/glue/dependent/GenericDependentResource.java index 8cfb09f..3f72a94 100644 --- a/src/main/java/io/csviri/operator/glue/dependent/GenericDependentResource.java +++ b/src/main/java/io/csviri/operator/glue/dependent/GenericDependentResource.java @@ -25,20 +25,24 @@ public class GenericDependentResource private final boolean clusterScoped; // optimize share between instances - private final GenericTemplateHandler genericTemplateHandler = new GenericTemplateHandler(); + private final GenericTemplateHandler genericTemplateHandler; - public GenericDependentResource(GenericKubernetesResource desired, String name, + public GenericDependentResource(GenericTemplateHandler genericTemplateHandler, + GenericKubernetesResource desired, String name, boolean clusterScoped) { super(new GroupVersionKind(desired.getApiVersion(), desired.getKind())); this.desired = desired; this.desiredTemplate = null; this.name = name; this.clusterScoped = clusterScoped; + this.genericTemplateHandler = genericTemplateHandler; } - public GenericDependentResource(String desiredTemplate, String name, boolean clusterScoped) { + public GenericDependentResource(GenericTemplateHandler genericTemplateHandler, + String desiredTemplate, String name, boolean clusterScoped) { super(new GroupVersionKind(Utils.getApiVersionFromTemplate(desiredTemplate), Utils.getKindFromTemplate(desiredTemplate))); + this.genericTemplateHandler = genericTemplateHandler; this.name = name; this.desiredTemplate = desiredTemplate; this.desired = null; diff --git a/src/main/java/io/csviri/operator/glue/reconciler/glue/GlueReconciler.java b/src/main/java/io/csviri/operator/glue/reconciler/glue/GlueReconciler.java index 65513b5..d410a97 100644 --- a/src/main/java/io/csviri/operator/glue/reconciler/glue/GlueReconciler.java +++ b/src/main/java/io/csviri/operator/glue/reconciler/glue/GlueReconciler.java @@ -49,13 +49,14 @@ public class GlueReconciler implements Reconciler, Cleaner, ErrorSta private final KubernetesResourceDeletedCondition deletePostCondition = new KubernetesResourceDeletedCondition(); - private final GenericTemplateHandler genericTemplateHandler = new GenericTemplateHandler(); - + private final GenericTemplateHandler genericTemplateHandler; public GlueReconciler(ValidationAndErrorHandler validationAndErrorHandler, - InformerRegister informerRegister) { + InformerRegister informerRegister, + GenericTemplateHandler genericTemplateHandler) { this.validationAndErrorHandler = validationAndErrorHandler; this.informerRegister = informerRegister; + this.genericTemplateHandler = genericTemplateHandler; } /** @@ -131,7 +132,6 @@ private void registerRelatedResourceInformers(Context context, // todo test private void cleanupRemovedResourcesFromWorkflow(Context context, Glue primary) { - context.getSecondaryResources(GenericKubernetesResource.class).forEach(r -> { String dependentName = r.getMetadata().getAnnotations().get(DEPENDENT_NAME_ANNOTATION_KEY); // dependent name is null for related resources @@ -200,21 +200,23 @@ private void createAndAddDependentToWorkflow(Glue primary, Context context .ifPresent(c -> builder.withReconcilePrecondition(toCondition(c))); } - private static GenericDependentResource createDependentResource(DependentResourceSpec spec, + private GenericDependentResource createDependentResource(DependentResourceSpec spec, boolean leafDependent, Boolean resourceInSameNamespaceAsPrimary) { if (leafDependent && resourceInSameNamespaceAsPrimary && !spec.isClusterScoped()) { return spec.getResourceTemplate() != null - ? new GCGenericDependentResource(spec.getResourceTemplate(), spec.getName(), + ? new GCGenericDependentResource(genericTemplateHandler, spec.getResourceTemplate(), + spec.getName(), spec.isClusterScoped()) - : new GCGenericDependentResource(spec.getResource(), spec.getName(), + : new GCGenericDependentResource(genericTemplateHandler, spec.getResource(), + spec.getName(), spec.isClusterScoped()); } else { return spec.getResourceTemplate() != null - ? new GenericDependentResource(spec.getResourceTemplate(), spec.getName(), - spec.isClusterScoped()) - : new GenericDependentResource(spec.getResource(), spec.getName(), - spec.isClusterScoped()); + ? new GenericDependentResource(genericTemplateHandler, + spec.getResourceTemplate(), spec.getName(), spec.isClusterScoped()) + : new GenericDependentResource(genericTemplateHandler, + spec.getResource(), spec.getName(), spec.isClusterScoped()); } } diff --git a/src/main/java/io/csviri/operator/glue/reconciler/operator/GlueOperatorReconciler.java b/src/main/java/io/csviri/operator/glue/reconciler/operator/GlueOperatorReconciler.java index 5fb51ae..63d2e89 100644 --- a/src/main/java/io/csviri/operator/glue/reconciler/operator/GlueOperatorReconciler.java +++ b/src/main/java/io/csviri/operator/glue/reconciler/operator/GlueOperatorReconciler.java @@ -15,7 +15,9 @@ import io.csviri.operator.glue.customresource.operator.GlueOperatorSpec; import io.csviri.operator.glue.customresource.operator.ResourceFlowOperatorStatus; import io.csviri.operator.glue.reconciler.ValidationAndErrorHandler; +import io.csviri.operator.glue.templating.GenericTemplateHandler; import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; @@ -26,7 +28,6 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import jakarta.annotation.PostConstruct; -import jakarta.inject.Inject; import static io.csviri.operator.glue.reconciler.glue.GlueReconciler.GLUE_RECONCILER_NAME; @@ -42,19 +43,25 @@ public class GlueOperatorReconciler public static final String PARENT_RELATED_RESOURCE_NAME = "parent"; public static final String GLUE_OPERATOR_RECONCILER_NAME = "glue-operator"; - @Inject - ValidationAndErrorHandler validationAndErrorHandler; - @ConfigProperty(name = "quarkus.operator-sdk.controllers." + GLUE_RECONCILER_NAME + ".selector") Optional glueLabelSelector; - @Inject - ControllerConfig controllerConfig; + private final ControllerConfig controllerConfig; + private final ValidationAndErrorHandler validationAndErrorHandler; + private final GenericTemplateHandler genericTemplateHandler; private Map defaultGlueLabels; private InformerEventSource glueEventSource; + public GlueOperatorReconciler(ControllerConfig controllerConfig, + ValidationAndErrorHandler validationAndErrorHandler, + GenericTemplateHandler genericTemplateHandler) { + this.controllerConfig = controllerConfig; + this.validationAndErrorHandler = validationAndErrorHandler; + this.genericTemplateHandler = genericTemplateHandler; + } + @PostConstruct void init() { defaultGlueLabels = initDefaultLabelsToAddToGlue(); @@ -94,12 +101,10 @@ private Glue createGlue(GenericKubernetesResource targetParentResource, GlueOperator glueOperator) { var glue = new Glue(); - glue.setMetadata(new ObjectMetaBuilder() - .withName( - glueName(targetParentResource.getMetadata().getName(), targetParentResource.getKind())) - .withNamespace(targetParentResource.getMetadata().getNamespace()) - .withLabels(Map.of(FOR_GLUE_OPERATOR_LABEL_KEY, FOR_GLUE_OPERATOR_LABEL_VALUE)) - .build()); + ObjectMeta glueMetadata = glueMetadata(glueOperator, targetParentResource); + + + glue.setMetadata(glueMetadata); glue.setSpec(toWorkflowSpec(glueOperator.getSpec())); if (!defaultGlueLabels.isEmpty()) { @@ -113,13 +118,38 @@ private Glue createGlue(GenericKubernetesResource targetParentResource, parentRelatedSpec.setKind(parent.getKind()); 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; } + private ObjectMeta glueMetadata(GlueOperator glueOperator, + GenericKubernetesResource parent) { + + ObjectMetaBuilder objectMetaBuilder = new ObjectMetaBuilder(); + + var glueMeta = glueOperator.getSpec().getGlueMetadata(); + if (glueMeta != null) { + // optimize + var data = Map.of(PARENT_RELATED_RESOURCE_NAME, parent); + var glueName = genericTemplateHandler.processInputAndTemplate(data, glueMeta.getName()); + var glueNamespace = + genericTemplateHandler.processInputAndTemplate(data, glueMeta.getNamespace()); + objectMetaBuilder.withName(glueName); + objectMetaBuilder.withNamespace(glueNamespace); + } else { + objectMetaBuilder.withName( + glueName(parent.getMetadata().getName(), parent.getKind())) + .withNamespace(parent.getMetadata().getNamespace()); + } + + objectMetaBuilder + .withLabels(Map.of(FOR_GLUE_OPERATOR_LABEL_KEY, FOR_GLUE_OPERATOR_LABEL_VALUE)); + return objectMetaBuilder.build(); + } + private GlueSpec toWorkflowSpec(GlueOperatorSpec spec) { var res = new GlueSpec(); res.setChildResources(new ArrayList<>(spec.getChildResources())); diff --git a/src/main/java/io/csviri/operator/glue/templating/GenericTemplateHandler.java b/src/main/java/io/csviri/operator/glue/templating/GenericTemplateHandler.java index b257014..b8860cc 100644 --- a/src/main/java/io/csviri/operator/glue/templating/GenericTemplateHandler.java +++ b/src/main/java/io/csviri/operator/glue/templating/GenericTemplateHandler.java @@ -5,12 +5,16 @@ import io.csviri.operator.glue.Utils; import io.csviri.operator.glue.customresource.glue.Glue; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.quarkus.qute.Engine; import io.quarkus.qute.Template; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.inject.Singleton; + +@Singleton public class GenericTemplateHandler { public static final String WORKFLOW_METADATA_KEY = "glueMetadata"; @@ -18,18 +22,29 @@ public class GenericTemplateHandler { private static final ObjectMapper objectMapper = new ObjectMapper(); private static final Engine engine = Engine.builder().addDefaults().build(); + public String processTemplate(Map> data, String template) { + Template parsedTemplate = engine.parse(template); + return parsedTemplate.data(data).render(); + } + + 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))); + return processTemplate(res, template); + } + public String processTemplate(String template, Glue primary, Context context) { - Template hello = engine.parse(template); var data = createDataWithResources(primary, context); - return hello.data(data).render(); + return processTemplate(data, template); } - @SuppressWarnings("rawtypes") - private static Map createDataWithResources(Glue primary, + private static Map> createDataWithResources(Glue primary, Context context) { - Map res = new HashMap<>(); + Map> res = new HashMap<>(); var actualResourcesByName = Utils.getActualResourcesByNameInWorkflow(context, primary); actualResourcesByName.forEach((key, value) -> res.put(key, diff --git a/src/test/java/io/csviri/operator/glue/GlueOperatorTest.java b/src/test/java/io/csviri/operator/glue/GlueOperatorTest.java index a8ac01e..ac40c59 100644 --- a/src/test/java/io/csviri/operator/glue/GlueOperatorTest.java +++ b/src/test/java/io/csviri/operator/glue/GlueOperatorTest.java @@ -10,12 +10,15 @@ import io.csviri.operator.glue.customresource.TestCustomResource; import io.csviri.operator.glue.customresource.TestCustomResource2; import io.csviri.operator.glue.customresource.glue.DependentResourceSpec; +import io.csviri.operator.glue.customresource.glue.Glue; import io.csviri.operator.glue.customresource.operator.GlueOperator; import io.csviri.operator.glue.customresource.operator.GlueOperatorSpec; import io.csviri.operator.glue.customresource.operator.Parent; import io.csviri.operator.glue.reconciler.ValidationAndErrorHandler; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; import io.quarkus.test.junit.QuarkusTest; import static io.csviri.operator.glue.TestData.*; @@ -27,6 +30,8 @@ @QuarkusTest class GlueOperatorTest extends TestBase { + public static final String COPIED_SECRET_NAME = "copied-secret"; + @BeforeEach void applyCRD() { TestUtils.applyTestCrd(client, TestCustomResource.class, TestCustomResource2.class); @@ -188,6 +193,42 @@ void parentWithLabelSelector() { }); } + @Test + void secretCopySample() { + var secret = TestUtils.load("/sample/secretcopy/secret-to-copy.yaml", Secret.class); + client.resource(secret).createOr(NonDeletingOperation::update); + + var go = create(TestUtils + .loadGlueOperator("/sample/secretcopy/secret-copy.operator.yaml")); + + await().untilAsserted(() -> { + var namespaces = client.namespaces().list().getItems(); + namespaces.forEach(ns -> { + var copiedSecret = + client.secrets().inNamespace(ns.getMetadata().getName()).withName(COPIED_SECRET_NAME) + .get(); + assertThat(copiedSecret).isNotNull(); + assertThat(copiedSecret.getData().get("shared-password")) + .isEqualTo(secret.getData().get("password")); + }); + }); + + delete(go); + client.namespaces().list().getItems().forEach(ns -> { + client.resources(Glue.class) + .inNamespace(ns.getMetadata().getName()).withName("copied-secret-glue").delete(); + client.secrets() + .inNamespace(ns.getMetadata().getName()).withName(COPIED_SECRET_NAME).delete(); + }); + await().untilAsserted(() -> { + client.namespaces().list().getItems().forEach(ns -> { + var g = client.resources(Glue.class) + .inNamespace(ns.getMetadata().getName()).withName("copied-glue-secret").get(); + assertThat(g).isNull(); + }); + }); + } + GlueOperator testWorkflowOperator() { var wo = new GlueOperator(); diff --git a/src/test/java/io/csviri/operator/glue/TestUtils.java b/src/test/java/io/csviri/operator/glue/TestUtils.java index f5efe86..ae3dcd7 100644 --- a/src/test/java/io/csviri/operator/glue/TestUtils.java +++ b/src/test/java/io/csviri/operator/glue/TestUtils.java @@ -24,7 +24,7 @@ public class TestUtils { - public static final Duration GC_WAIT_TIMEOUT = Duration.ofSeconds(90); + public static final Duration GC_WAIT_TIMEOUT = Duration.ofSeconds(120); public static final Duration INITIAL_RECONCILE_WAIT_TIMEOUT = Duration.ofMillis(150); public static final int CRD_READY_WAIT = 1000; diff --git a/src/test/resources/sample/secretcopy/secret-copy.operator.yaml b/src/test/resources/sample/secretcopy/secret-copy.operator.yaml new file mode 100644 index 0000000..86ee9e5 --- /dev/null +++ b/src/test/resources/sample/secretcopy/secret-copy.operator.yaml @@ -0,0 +1,28 @@ +apiVersion: io.csviri.operator.glue/v1beta1 +kind: GlueOperator +metadata: + name: secret-copy-operator +spec: + parent: + apiVersion: v1 + kind: Namespace + clusterScoped: true + glueMetadata: + name: copied-secret-glue + namespace: "{parent.metadata.name}" + childResources: + - name: secret + resource: + apiVersion: v1 + kind: Secret + metadata: + name: copied-secret + type: Opaque + data: + shared-password: "{sharedsecret.data.password}" + relatedResources: + - name: sharedsecret + apiVersion: v1 + kind: Secret + namespace: default + resourceNames: [ "secret-to-copy" ] diff --git a/src/test/resources/sample/secretcopy/secret-to-copy.yaml b/src/test/resources/sample/secretcopy/secret-to-copy.yaml new file mode 100644 index 0000000..27f0cff --- /dev/null +++ b/src/test/resources/sample/secretcopy/secret-to-copy.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-to-copy + namespace: default +type: Opaque +data: + password: dG9wc2VjcmV0 +