diff --git a/docs/documentation/dependent-resources.md b/docs/documentation/dependent-resources.md index 06149cf8ba..7678e997e6 100644 --- a/docs/documentation/dependent-resources.md +++ b/docs/documentation/dependent-resources.md @@ -483,6 +483,16 @@ also be created, one per dependent resource. See [integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateBulkIT.java) as a sample. + +## GenericKubernetesResource based Dependent Resources + +In rare circumstances resource handling where there is no class representation or just typeless handling might be needed. +Fabric8 Client provides [GenericKubernetesResource](https://github.com/fabric8io/kubernetes-client/blob/main/doc/CHEATSHEET.md#resource-typeless-api) +to support that. + +For dependent resource this is supported by [GenericKubernetesDependentResource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java#L8-L8) +. See samples [here](https://github.com/java-operator-sdk/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource). + ## Other Dependent Resource Features ### Caching and Event Handling in [KubernetesDependentResource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java) diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java index 61ff1562ee..b819bd0ca3 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java @@ -291,9 +291,9 @@ private static String getScope(ResourceID resourceID) { } private static void addGVKTags(GroupVersionKind gvk, List tags, boolean prefixed) { - addTagOmittingOnEmptyValue(GROUP, gvk.group, tags, prefixed); - addTag(VERSION, gvk.version, tags, prefixed); - addTag(KIND, gvk.kind, tags, prefixed); + addTagOmittingOnEmptyValue(GROUP, gvk.getGroup(), tags, prefixed); + addTag(VERSION, gvk.getVersion(), tags, prefixed); + addTag(KIND, gvk.getKind(), tags, prefixed); } private void incrementCounter(ResourceID id, String counterName, Map metadata, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java index 87d8a6cd80..f8ee9f4e84 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java @@ -3,6 +3,7 @@ import java.util.Optional; import java.util.Set; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.cache.ItemStore; import io.javaoperatorsdk.operator.ReconcilerUtils; @@ -28,7 +29,11 @@ protected DefaultResourceConfiguration(Class resourceClass, OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, ItemStore itemStore, Long informerListLimit) { this.resourceClass = resourceClass; - this.resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass); + this.resourceTypeName = resourceClass.isAssignableFrom(GenericKubernetesResource.class) + // in general this is irrelevant now for secondary resources it is used just by controller + // where GenericKubernetesResource now does not apply + ? GenericKubernetesResource.class.getSimpleName() + : ReconcilerUtils.getResourceTypeName(resourceClass); this.onAddFilter = onAddFilter; this.onUpdateFilter = onUpdateFilter; this.genericFilter = genericFilter; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index 4685fb1d57..62f345426c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -78,7 +78,7 @@ public static void executeAndWaitForAllToComplete(Stream stream, // to find out any exceptions f.get(); } catch (ExecutionException e) { - throw new OperatorException(e.getCause()); + throw new OperatorException(e); } catch (InterruptedException e) { log.warn("Interrupted.", e); Thread.currentThread().interrupt(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 4b0007c96e..bca39b189c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -4,12 +4,14 @@ import java.util.Optional; import java.util.Set; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.cache.ItemStore; import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.GroupVersionKind; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; @@ -30,9 +32,11 @@ class DefaultInformerConfiguration extends private final SecondaryToPrimaryMapper secondaryToPrimaryMapper; private final boolean followControllerNamespaceChanges; private final OnDeleteFilter onDeleteFilter; + private final GroupVersionKind groupVersionKind; protected DefaultInformerConfiguration(String labelSelector, Class resourceClass, + GroupVersionKind groupVersionKind, PrimaryToSecondaryMapper primaryToSecondaryMapper, SecondaryToPrimaryMapper secondaryToPrimaryMapper, Set namespaces, boolean followControllerNamespaceChanges, @@ -44,7 +48,7 @@ protected DefaultInformerConfiguration(String labelSelector, super(resourceClass, namespaces, labelSelector, onAddFilter, onUpdateFilter, genericFilter, itemStore, informerListLimit); this.followControllerNamespaceChanges = followControllerNamespaceChanges; - + this.groupVersionKind = groupVersionKind; this.primaryToSecondaryMapper = primaryToSecondaryMapper; this.secondaryToPrimaryMapper = Objects.requireNonNullElse(secondaryToPrimaryMapper, @@ -72,6 +76,10 @@ public Optional> onDeleteFilter() { public

PrimaryToSecondaryMapper

getPrimaryToSecondaryMapper() { return (PrimaryToSecondaryMapper

) primaryToSecondaryMapper; } + + public Optional getGroupVersionKind() { + return Optional.ofNullable(groupVersionKind); + } } /** @@ -109,14 +117,17 @@ public

PrimaryToSecondaryMapper

getPrimaryToSecondary

PrimaryToSecondaryMapper

getPrimaryToSecondaryMapper(); + Optional getGroupVersionKind(); + @SuppressWarnings("unused") class InformerConfigurationBuilder { + private final Class resourceClass; + private final GroupVersionKind groupVersionKind; private PrimaryToSecondaryMapper primaryToSecondaryMapper; private SecondaryToPrimaryMapper secondaryToPrimaryMapper; private Set namespaces; private String labelSelector; - private final Class resourceClass; private OnAddFilter onAddFilter; private OnUpdateFilter onUpdateFilter; private OnDeleteFilter onDeleteFilter; @@ -127,6 +138,13 @@ class InformerConfigurationBuilder { private InformerConfigurationBuilder(Class resourceClass) { this.resourceClass = resourceClass; + this.groupVersionKind = null; + } + + @SuppressWarnings("unchecked") + private InformerConfigurationBuilder(GroupVersionKind groupVersionKind) { + this.resourceClass = (Class) GenericKubernetesResource.class; + this.groupVersionKind = groupVersionKind; } public

InformerConfigurationBuilder withPrimaryToSecondaryMapper( @@ -244,7 +262,7 @@ public InformerConfigurationBuilder withInformerListLimit(Long informerListLi } public InformerConfiguration build() { - return new DefaultInformerConfiguration<>(labelSelector, resourceClass, + return new DefaultInformerConfiguration<>(labelSelector, resourceClass, groupVersionKind, primaryToSecondaryMapper, secondaryToPrimaryMapper, namespaces, inheritControllerNamespacesOnChange, onAddFilter, onUpdateFilter, @@ -257,6 +275,14 @@ static InformerConfigurationBuilder from( return new InformerConfigurationBuilder<>(resourceClass); } + /** + * * For the case when want to use {@link GenericKubernetesResource} + */ + static InformerConfigurationBuilder from( + GroupVersionKind groupVersionKind) { + return new InformerConfigurationBuilder<>(groupVersionKind); + } + /** * Creates a configuration builder that inherits namespaces from the controller and follows * namespaces changes. @@ -272,6 +298,16 @@ static InformerConfigurationBuilder from( .withNamespacesInheritedFromController(eventSourceContext); } + /** + * * For the case when want to use {@link GenericKubernetesResource} + */ + @SuppressWarnings("unchecked") + static InformerConfigurationBuilder from( + GroupVersionKind groupVersionKind, EventSourceContext eventSourceContext) { + return new InformerConfigurationBuilder(groupVersionKind) + .withNamespacesInheritedFromController(eventSourceContext); + } + @SuppressWarnings("unchecked") @Override default Class getResourceClass() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/GroupVersionKind.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/GroupVersionKind.java index 1a8a4b595d..9e5cbea14d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/GroupVersionKind.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/GroupVersionKind.java @@ -1,13 +1,27 @@ package io.javaoperatorsdk.operator.processing; +import java.util.Objects; + import io.fabric8.kubernetes.api.model.HasMetadata; public class GroupVersionKind { - public final String group; - public final String version; - public final String kind; + private final String group; + private final String version; + private final String kind; - GroupVersionKind(String group, String version, String kind) { + public GroupVersionKind(String apiVersion, String kind) { + this.kind = kind; + String[] groupAndVersion = apiVersion.split("/"); + if (groupAndVersion.length == 1) { + this.group = null; + this.version = groupAndVersion[0]; + } else { + this.group = groupAndVersion[0]; + this.version = groupAndVersion[1]; + } + } + + public GroupVersionKind(String group, String version, String kind) { this.group = group; this.version = version; this.kind = kind; @@ -17,4 +31,45 @@ public static GroupVersionKind gvkFor(Class resourceClass return new GroupVersionKind(HasMetadata.getGroup(resourceClass), HasMetadata.getVersion(resourceClass), HasMetadata.getKind(resourceClass)); } + + public String getGroup() { + return group; + } + + public String getVersion() { + return version; + } + + public String getKind() { + return kind; + } + + public String apiVersion() { + return group == null || group.isBlank() ? version : group + "/" + version; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + GroupVersionKind that = (GroupVersionKind) o; + return Objects.equals(group, that.group) && Objects.equals(version, that.version) + && Objects.equals(kind, that.kind); + } + + @Override + public int hashCode() { + return Objects.hash(group, version, kind); + } + + @Override + public String toString() { + return "GroupVersionKind{" + + "group='" + group + '\'' + + ", version='" + version + '\'' + + ", kind='" + kind + '\'' + + '}'; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java new file mode 100644 index 0000000000..98f2346577 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesDependentResource.java @@ -0,0 +1,25 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.processing.GroupVersionKind; + +public class GenericKubernetesDependentResource

+ extends KubernetesDependentResource { + + private GroupVersionKind groupVersionKind; + + public GenericKubernetesDependentResource(GroupVersionKind groupVersionKind) { + super(GenericKubernetesResource.class); + this.groupVersionKind = groupVersionKind; + } + + protected InformerConfiguration.InformerConfigurationBuilder informerConfigurationBuilder() { + return InformerConfiguration.from(groupVersionKind); + } + + public GroupVersionKind getGroupVersionKind() { + return groupVersionKind; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index b587c72327..a4305df502 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -67,14 +67,18 @@ private void configureWith(String labelSelector, Set namespaces, namespaces = context.getControllerConfiguration().getNamespaces(); } - var ic = InformerConfiguration.from(resourceType()) + var ic = informerConfigurationBuilder() .withLabelSelector(labelSelector) .withSecondaryToPrimaryMapper(getSecondaryToPrimaryMapper()) .withNamespaces(namespaces, inheritNamespacesOnChange) .build(); configureWith(new InformerEventSource<>(ic, context)); + } + // just to seamlessly handle GenericKubernetesDependentResource + protected InformerConfiguration.InformerConfigurationBuilder informerConfigurationBuilder() { + return InformerConfiguration.from(resourceType()); } @SuppressWarnings("unchecked") diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index dfaec7c19e..81d31f7407 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; @@ -88,7 +89,11 @@ public InformerEventSource(InformerConfiguration configuration, KubernetesCli public InformerEventSource(InformerConfiguration configuration, KubernetesClient client, boolean parseResourceVersions) { - super(client.resources(configuration.getResourceClass()), configuration, parseResourceVersions); + super( + configuration.getGroupVersionKind() + .map(gvk -> client.genericKubernetesResources(gvk.apiVersion(), gvk.getKind())) + .orElseGet(() -> (MixedOperation) client.resources(configuration.getResourceClass())), + configuration, parseResourceVersions); // If there is a primary to secondary mapper there is no need for primary to secondary index. primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper(); if (primaryToSecondaryMapper == null) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index 639eac1a59..28cf33a041 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -13,6 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.ExceptionHandler; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; @@ -113,6 +114,9 @@ public void start() throws OperatorException { private String versionedFullResourceName() { final var apiTypeClass = informer.getApiTypeClass(); + if (apiTypeClass.isAssignableFrom(GenericKubernetesResource.class)) { + return GenericKubernetesResource.class.getSimpleName(); + } return ReconcilerUtils.getResourceTypeNameWithVersion(apiTypeClass); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 7a1a6ba310..64d402ada5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -9,9 +9,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ConfigurationService; @@ -35,15 +33,15 @@ public abstract class ManagedInformerEventSource cache; - private boolean parseResourceVersions; + private final boolean parseResourceVersions; private ConfigurationService configurationService; - private C configuration; + private final C configuration; private Map>> indexers = new HashMap<>(); protected TemporaryResourceCache temporaryResourceCache; - protected MixedOperation, Resource> client; + protected MixedOperation client; protected ManagedInformerEventSource( - MixedOperation, Resource> client, C configuration, + MixedOperation client, C configuration, boolean parseResourceVersions) { super(configuration.getResourceClass()); this.parseResourceVersions = parseResourceVersions; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/GroupVersionKindTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/GroupVersionKindTest.java new file mode 100644 index 0000000000..ad69cae800 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/GroupVersionKindTest.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.processing; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class GroupVersionKindTest { + + @Test + void testInitFromApiVersion() { + var gvk = new GroupVersionKind("v1", "ConfigMap"); + assertThat(gvk.getGroup()).isNull(); + assertThat(gvk.getVersion()).isEqualTo("v1"); + + gvk = new GroupVersionKind("apps/v1", "Deployment"); + assertThat(gvk.getGroup()).isEqualTo("apps"); + assertThat(gvk.getVersion()).isEqualTo("v1"); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentManagedIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentManagedIT.java new file mode 100644 index 0000000000..f06185d402 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentManagedIT.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.GenericKubernetesDependentSpec; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentresourcemanaged.GenericKubernetesDependentManagedCustomResource; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentresourcemanaged.GenericKubernetesDependentManagedReconciler; + +public class GenericKubernetesDependentManagedIT + extends GenericKubernetesDependentTestBase { + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new GenericKubernetesDependentManagedReconciler()) + .build(); + + @Override + public LocallyRunOperatorExtension extension() { + return extension; + } + + @Override + GenericKubernetesDependentManagedCustomResource testResource(String name, String data) { + var resource = new GenericKubernetesDependentManagedCustomResource(); + resource.setMetadata(new ObjectMetaBuilder() + .withName(name) + .build()); + resource.setSpec(new GenericKubernetesDependentSpec()); + resource.getSpec().setValue(INITIAL_DATA); + return resource; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentStandaloneIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentStandaloneIT.java new file mode 100644 index 0000000000..708d2f119c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentStandaloneIT.java @@ -0,0 +1,35 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.GenericKubernetesDependentSpec; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentstandalone.GenericKubernetesDependentStandaloneCustomResource; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentstandalone.GenericKubernetesDependentStandaloneReconciler; + +public class GenericKubernetesDependentStandaloneIT + extends GenericKubernetesDependentTestBase { + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new GenericKubernetesDependentStandaloneReconciler()) + .build(); + + @Override + public LocallyRunOperatorExtension extension() { + return extension; + } + + @Override + GenericKubernetesDependentStandaloneCustomResource testResource(String name, String data) { + var resource = new GenericKubernetesDependentStandaloneCustomResource(); + resource.setMetadata(new ObjectMetaBuilder() + .withName(name) + .build()); + resource.setSpec(new GenericKubernetesDependentSpec()); + resource.getSpec().setValue(INITIAL_DATA); + return resource; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentTestBase.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentTestBase.java new file mode 100644 index 0000000000..fea23b7796 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesDependentTestBase.java @@ -0,0 +1,52 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.GenericKubernetesDependentSpec; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentstandalone.ConfigMapGenericKubernetesDependent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public abstract class GenericKubernetesDependentTestBase> { + + public static final String INITIAL_DATA = "Initial data"; + public static final String CHANGED_DATA = "Changed data"; + public static final String TEST_RESOURCE_NAME = "test1"; + + @Test + void testReconciliation() { + var resource = extension().create(testResource(TEST_RESOURCE_NAME, INITIAL_DATA)); + + await().untilAsserted(() -> { + var cm = extension().get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(cm).isNotNull(); + assertThat(cm.getData()).containsEntry(ConfigMapGenericKubernetesDependent.KEY, INITIAL_DATA); + }); + + resource.getSpec().setValue(CHANGED_DATA); + resource = extension().replace(resource); + + await().untilAsserted(() -> { + var cm = extension().get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(cm.getData()).containsEntry(ConfigMapGenericKubernetesDependent.KEY, CHANGED_DATA); + }); + + extension().delete(resource); + + await().timeout(Duration.ofSeconds(30)).untilAsserted(() -> { + var cm = extension().get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(cm).isNull(); + }); + } + + public abstract LocallyRunOperatorExtension extension(); + + abstract R testResource(String name, String data); + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesResourceHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesResourceHandlingIT.java new file mode 100644 index 0000000000..b3ee935553 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/GenericKubernetesResourceHandlingIT.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.GenericKubernetesDependentSpec; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesresourcehandling.GenericKubernetesResourceHandlingCustomResource; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesresourcehandling.GenericKubernetesResourceHandlingReconciler; + +public class GenericKubernetesResourceHandlingIT + extends GenericKubernetesDependentTestBase { + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new GenericKubernetesResourceHandlingReconciler()) + .build(); + + @Override + public LocallyRunOperatorExtension extension() { + return extension; + } + + @Override + GenericKubernetesResourceHandlingCustomResource testResource(String name, String data) { + var resource = new GenericKubernetesResourceHandlingCustomResource(); + resource.setMetadata(new ObjectMetaBuilder() + .withName(name) + .build()); + resource.setSpec(new GenericKubernetesDependentSpec()); + resource.getSpec().setValue(INITIAL_DATA); + return resource; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/GenericKubernetesDependentSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/GenericKubernetesDependentSpec.java new file mode 100644 index 0000000000..050e0cf520 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/GenericKubernetesDependentSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource; + +public class GenericKubernetesDependentSpec { + + private String value; + + public String getValue() { + return value; + } + + public GenericKubernetesDependentSpec setValue(String value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/ConfigMapGenericKubernetesDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/ConfigMapGenericKubernetesDependent.java new file mode 100644 index 0000000000..d5ab470cfb --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/ConfigMapGenericKubernetesDependent.java @@ -0,0 +1,46 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentresourcemanaged; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; +import io.javaoperatorsdk.operator.processing.GroupVersionKind; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesDependentResource; + +public class ConfigMapGenericKubernetesDependent extends + GenericKubernetesDependentResource + implements + Creator, + Updater, + GarbageCollected { + + public static final String VERSION = "v1"; + public static final String KIND = "ConfigMap"; + public static final String KEY = "key"; + + public ConfigMapGenericKubernetesDependent() { + super(new GroupVersionKind("", VERSION, KIND)); + } + + @Override + protected GenericKubernetesResource desired( + GenericKubernetesDependentManagedCustomResource primary, + Context context) { + + try (InputStream is = this.getClass().getResourceAsStream("/configmap.yaml")) { + var res = context.getClient().genericKubernetesResources(VERSION, KIND).load(is).item(); + res.getMetadata().setName(primary.getMetadata().getName()); + res.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + Map data = (Map) res.getAdditionalProperties().get("data"); + data.put(KEY, primary.getSpec().getValue()); + return res; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedCustomResource.java new file mode 100644 index 0000000000..d7c44ddab3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedCustomResource.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentresourcemanaged; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.GenericKubernetesDependentSpec; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("gkdm") +public class GenericKubernetesDependentManagedCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedReconciler.java new file mode 100644 index 0000000000..64651ec23e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentresourcemanaged/GenericKubernetesDependentManagedReconciler.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentresourcemanaged; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@ControllerConfiguration( + dependents = {@Dependent(type = ConfigMapGenericKubernetesDependent.class)}) +public class GenericKubernetesDependentManagedReconciler + implements Reconciler { + + @Override + public UpdateControl reconcile( + GenericKubernetesDependentManagedCustomResource resource, + Context context) { + + return UpdateControl.noUpdate(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/ConfigMapGenericKubernetesDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/ConfigMapGenericKubernetesDependent.java new file mode 100644 index 0000000000..4efc968ef0 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/ConfigMapGenericKubernetesDependent.java @@ -0,0 +1,46 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentstandalone; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; +import io.javaoperatorsdk.operator.processing.GroupVersionKind; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesDependentResource; + +public class ConfigMapGenericKubernetesDependent extends + GenericKubernetesDependentResource + implements + Creator, + Updater, + GarbageCollected { + + public static final String VERSION = "v1"; + public static final String KIND = "ConfigMap"; + public static final String KEY = "key"; + + public ConfigMapGenericKubernetesDependent() { + super(new GroupVersionKind("", VERSION, KIND)); + } + + @Override + protected GenericKubernetesResource desired( + GenericKubernetesDependentStandaloneCustomResource primary, + Context context) { + + try (InputStream is = this.getClass().getResourceAsStream("/configmap.yaml")) { + var res = context.getClient().genericKubernetesResources(VERSION, KIND).load(is).item(); + res.getMetadata().setName(primary.getMetadata().getName()); + res.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + Map data = (Map) res.getAdditionalProperties().get("data"); + data.put(KEY, primary.getSpec().getValue()); + return res; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneCustomResource.java new file mode 100644 index 0000000000..38eaf804e4 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneCustomResource.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentstandalone; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.GenericKubernetesDependentSpec; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("gkd") +public class GenericKubernetesDependentStandaloneCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneReconciler.java new file mode 100644 index 0000000000..1969ad8f2a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesdependentstandalone/GenericKubernetesDependentStandaloneReconciler.java @@ -0,0 +1,33 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesdependentstandalone; + +import java.util.Map; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +@ControllerConfiguration +public class GenericKubernetesDependentStandaloneReconciler + implements Reconciler, + EventSourceInitializer { + + private final ConfigMapGenericKubernetesDependent dependent = + new ConfigMapGenericKubernetesDependent(); + + public GenericKubernetesDependentStandaloneReconciler() {} + + @Override + public UpdateControl reconcile( + GenericKubernetesDependentStandaloneCustomResource resource, + Context context) { + + dependent.reconcile(resource, context); + + return UpdateControl.noUpdate(); + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + return EventSourceInitializer.nameEventSources(dependent.eventSource(context).orElseThrow()); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesresourcehandling/GenericKubernetesResourceHandlingCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesresourcehandling/GenericKubernetesResourceHandlingCustomResource.java new file mode 100644 index 0000000000..47b3a14a5a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesresourcehandling/GenericKubernetesResourceHandlingCustomResource.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesresourcehandling; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; +import io.javaoperatorsdk.operator.sample.generickubernetesresource.GenericKubernetesDependentSpec; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("gkrr") +public class GenericKubernetesResourceHandlingCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesresourcehandling/GenericKubernetesResourceHandlingReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesresourcehandling/GenericKubernetesResourceHandlingReconciler.java new file mode 100644 index 0000000000..45be0281f6 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/generickubernetesresource/generickubernetesresourcehandling/GenericKubernetesResourceHandlingReconciler.java @@ -0,0 +1,77 @@ +package io.javaoperatorsdk.operator.sample.generickubernetesresource.generickubernetesresourcehandling; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.GroupVersionKind; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +@ControllerConfiguration +public class GenericKubernetesResourceHandlingReconciler + implements Reconciler, + EventSourceInitializer { + + + public static final String VERSION = "v1"; + public static final String KIND = "ConfigMap"; + public static final String KEY = "key"; + + @Override + public UpdateControl reconcile( + GenericKubernetesResourceHandlingCustomResource primary, + Context context) { + + var secondary = context.getSecondaryResource(GenericKubernetesResource.class); + + secondary.ifPresentOrElse(r -> { + var desired = desiredConfigMap(primary, context); + if (!matches(r, desired)) { + context.getClient().genericKubernetesResources(VERSION, KIND).resource(desired).update(); + } + }, () -> context.getClient().genericKubernetesResources(VERSION, KIND) + .resource(desiredConfigMap(primary, context)).create()); + + return UpdateControl.noUpdate(); + } + + @SuppressWarnings("unchecked") + private boolean matches(GenericKubernetesResource actual, GenericKubernetesResource desired) { + var actualData = (HashMap) actual.getAdditionalProperties().get("data"); + var desiredData = (HashMap) desired.getAdditionalProperties().get("data"); + return actualData.equals(desiredData); + } + + GenericKubernetesResource desiredConfigMap( + GenericKubernetesResourceHandlingCustomResource primary, + Context context) { + try (InputStream is = this.getClass().getResourceAsStream("/configmap.yaml")) { + var res = context.getClient().genericKubernetesResources(VERSION, KIND).load(is).item(); + res.getMetadata().setName(primary.getMetadata().getName()); + res.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + Map data = (Map) res.getAdditionalProperties().get("data"); + data.put(KEY, primary.getSpec().getValue()); + res.addOwnerReference(primary); + return res; + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + + @Override + public Map prepareEventSources( + EventSourceContext context) { + + var informerEventSource = new InformerEventSource<>(InformerConfiguration.from( + new GroupVersionKind("", VERSION, KIND), context).build(), + context); + + return EventSourceInitializer.nameEventSources(informerEventSource); + } +} diff --git a/operator-framework/src/test/resources/configmap.yaml b/operator-framework/src/test/resources/configmap.yaml new file mode 100644 index 0000000000..3245e257ab --- /dev/null +++ b/operator-framework/src/test/resources/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "" +data: + key: "" \ No newline at end of file