diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java index 6876fb0f8a..c28aeefc49 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtils.java @@ -16,8 +16,16 @@ package io.javaoperatorsdk.operator.api.reconciler; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Predicate; import java.util.function.UnaryOperator; +import java.util.stream.Collector; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -364,13 +372,13 @@ public static R resourcePatch( if (esList.isEmpty()) { throw new IllegalStateException("No event source found for type: " + resource.getClass()); } + var es = esList.get(0); if (esList.size() > 1) { - throw new IllegalStateException( - "Multiple event sources found for: " - + resource.getClass() - + " please provide the target event source"); + log.warn( + "Multiple event sources found for type: {}, selecting first with name {}", + resource.getClass(), + es.name()); } - var es = esList.get(0); if (es instanceof ManagedInformerEventSource mes) { return resourcePatch(resource, updateOperation, mes); } else { @@ -714,4 +722,56 @@ private static int validateResourceVersion(String v1) { } return v1Length; } + + /** + * Returns a collector that deduplicates Kubernetes objects by keeping only the one with the + * latest metadata.resourceVersion for each unique name and namespace combination. The intended + * use case is for the rather rare setup when there are overlapping {@link + * io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s for a + * resource type. + * + * @param the type of HasMetadata objects + * @return a collector that produces a collection of deduplicated Kubernetes objects + */ + public static Collector> latestDistinct() { + return Collectors.collectingAndThen(latestDistinctToMap(), Map::values); + } + + /** + * Returns a collector that deduplicates Kubernetes objects by keeping only the one with the + * latest metadata.resourceVersion for each unique name and namespace combination. The intended + * use case is for the rather rare setup when there are overlapping {@link + * io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s for a + * resource type. + * + * @param the type of HasMetadata objects + * @return a collector that produces a List of deduplicated Kubernetes objects + */ + public static Collector> latestDistinctList() { + return Collectors.collectingAndThen( + latestDistinctToMap(), map -> new ArrayList<>(map.values())); + } + + /** + * Returns a collector that deduplicates Kubernetes objects by keeping only the one with the + * latest metadata.resourceVersion for each unique name and namespace combination. The intended + * use case is for the rather rare setup when there are overlapping {@link + * io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}s for a + * resource type. + * + * @param the type of HasMetadata objects + * @return a collector that produces a Set of deduplicated Kubernetes objects + */ + public static Collector> latestDistinctSet() { + return Collectors.collectingAndThen(latestDistinctToMap(), map -> new HashSet<>(map.values())); + } + + private static Collector> latestDistinctToMap() { + return Collectors.toMap( + resource -> + new ResourceID(resource.getMetadata().getName(), resource.getMetadata().getNamespace()), + resource -> resource, + (existing, replacement) -> + compareResourceVersions(existing, replacement) >= 0 ? existing : replacement); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java index 6d8c244c83..7956f072cb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/reconciler/ReconcileUtilsTest.java @@ -15,9 +15,12 @@ */ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.UnaryOperator; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -27,6 +30,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; @@ -402,7 +406,7 @@ void resourcePatchThrowsWhenNoEventSourceFound() { } @Test - void resourcePatchThrowsWhenMultipleEventSourcesFound() { + void resourcePatchUsesFirstEventSourceIfMultipleEventSourcesPresent() { var resource = TestUtils.testCustomResource1(); var eventSourceRetriever = mock(EventSourceRetriever.class); var eventSource1 = mock(ManagedInformerEventSource.class); @@ -412,13 +416,10 @@ void resourcePatchThrowsWhenMultipleEventSourcesFound() { when(eventSourceRetriever.getEventSourcesFor(TestCustomResource.class)) .thenReturn(List.of(eventSource1, eventSource2)); - var exception = - assertThrows( - IllegalStateException.class, - () -> ReconcileUtils.resourcePatch(context, resource, UnaryOperator.identity())); + ReconcileUtils.resourcePatch(context, resource, UnaryOperator.identity()); - assertThat(exception.getMessage()).contains("Multiple event sources found for"); - assertThat(exception.getMessage()).contains("please provide the target event source"); + verify(eventSource1, times(1)) + .eventFilteringUpdateAndCacheResource(any(), any(UnaryOperator.class)); } @Test @@ -440,6 +441,269 @@ void resourcePatchThrowsWhenEventSourceIsNotManagedInformer() { assertThat(exception.getMessage()).contains("ManagedInformerEventSource"); } + @Test + void latestDistinctKeepsOnlyLatestResourceVersion() { + // Create multiple resources with same name and namespace but different versions + HasMetadata pod1v1 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("100") + .build()) + .build(); + + HasMetadata pod1v2 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("200") + .build()) + .build(); + + HasMetadata pod1v3 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("150") + .build()) + .build(); + + // Create a resource with different name + HasMetadata pod2v1 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod2") + .withNamespace("default") + .withResourceVersion("100") + .build()) + .build(); + + // Create a resource with same name but different namespace + HasMetadata pod1OtherNsv1 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("other") + .withResourceVersion("50") + .build()) + .build(); + + Collection result = + Stream.of(pod1v1, pod1v2, pod1v3, pod2v1, pod1OtherNsv1) + .collect(ReconcileUtils.latestDistinct()); + + // Should have 3 resources: pod1 in default (latest version 200), pod2 in default, and pod1 in + // other + assertThat(result).hasSize(3); + + // Find pod1 in default namespace - should have version 200 + HasMetadata pod1InDefault = + result.stream() + .filter( + r -> + "pod1".equals(r.getMetadata().getName()) + && "default".equals(r.getMetadata().getNamespace())) + .findFirst() + .orElseThrow(); + assertThat(pod1InDefault.getMetadata().getResourceVersion()).isEqualTo("200"); + + // Find pod2 in default namespace - should exist + HasMetadata pod2InDefault = + result.stream() + .filter( + r -> + "pod2".equals(r.getMetadata().getName()) + && "default".equals(r.getMetadata().getNamespace())) + .findFirst() + .orElseThrow(); + assertThat(pod2InDefault.getMetadata().getResourceVersion()).isEqualTo("100"); + + // Find pod1 in other namespace - should exist + HasMetadata pod1InOther = + result.stream() + .filter( + r -> + "pod1".equals(r.getMetadata().getName()) + && "other".equals(r.getMetadata().getNamespace())) + .findFirst() + .orElseThrow(); + assertThat(pod1InOther.getMetadata().getResourceVersion()).isEqualTo("50"); + } + + @Test + void latestDistinctHandlesEmptyStream() { + Collection result = + Stream.empty().collect(ReconcileUtils.latestDistinct()); + + assertThat(result).isEmpty(); + } + + @Test + void latestDistinctHandlesSingleResource() { + HasMetadata pod = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("100") + .build()) + .build(); + + Collection result = Stream.of(pod).collect(ReconcileUtils.latestDistinct()); + + assertThat(result).hasSize(1); + assertThat(result).contains(pod); + } + + @Test + void latestDistinctComparesNumericVersionsCorrectly() { + // Test that version 1000 is greater than version 999 (not lexicographic) + HasMetadata podV999 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("999") + .build()) + .build(); + + HasMetadata podV1000 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("1000") + .build()) + .build(); + + Collection result = + Stream.of(podV999, podV1000).collect(ReconcileUtils.latestDistinct()); + + assertThat(result).hasSize(1); + HasMetadata resultPod = result.iterator().next(); + assertThat(resultPod.getMetadata().getResourceVersion()).isEqualTo("1000"); + } + + @Test + void latestDistinctListReturnsListType() { + Pod pod1v1 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("100") + .build()) + .build(); + + Pod pod1v2 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("200") + .build()) + .build(); + + Pod pod2v1 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod2") + .withNamespace("default") + .withResourceVersion("100") + .build()) + .build(); + + List result = + Stream.of(pod1v1, pod1v2, pod2v1).collect(ReconcileUtils.latestDistinctList()); + + assertThat(result).isInstanceOf(List.class); + assertThat(result).hasSize(2); + + // Verify the list contains the correct resources + Pod pod1 = + result.stream() + .filter(r -> "pod1".equals(r.getMetadata().getName())) + .findFirst() + .orElseThrow(); + assertThat(pod1.getMetadata().getResourceVersion()).isEqualTo("200"); + } + + @Test + void latestDistinctSetReturnsSetType() { + Pod pod1v1 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("100") + .build()) + .build(); + + Pod pod1v2 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod1") + .withNamespace("default") + .withResourceVersion("200") + .build()) + .build(); + + Pod pod2v1 = + new PodBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName("pod2") + .withNamespace("default") + .withResourceVersion("100") + .build()) + .build(); + + Set result = Stream.of(pod1v1, pod1v2, pod2v1).collect(ReconcileUtils.latestDistinctSet()); + + assertThat(result).isInstanceOf(java.util.Set.class); + assertThat(result).hasSize(2); + + // Verify the set contains the correct resources + Pod pod1 = + result.stream() + .filter(r -> "pod1".equals(r.getMetadata().getName())) + .findFirst() + .orElseThrow(); + assertThat(pod1.getMetadata().getResourceVersion()).isEqualTo("200"); + } + + @Test + void latestDistinctListHandlesEmptyStream() { + List result = + Stream.empty().collect(ReconcileUtils.latestDistinctList()); + + assertThat(result).isEmpty(); + } + + @Test + void latestDistinctSetHandlesEmptyStream() { + Set result = + Stream.empty().collect(ReconcileUtils.latestDistinctSet()); + + assertThat(result).isEmpty(); + } + // naive performance test that compares the work case scenario for the parsing and non-parsing // variants @Test diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctIT.java new file mode 100644 index 0000000000..c66b0dc4de --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctIT.java @@ -0,0 +1,125 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.baseapi.latestdistinct; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.annotation.Sample; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; + +import static io.javaoperatorsdk.operator.baseapi.latestdistinct.LatestDistinctTestReconciler.LABEL_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@Sample( + tldr = "Latest Distinct with Multiple InformerEventSources", + description = + """ + Demonstrates using two separate InformerEventSource instances for ConfigMaps with \ + overlapping watches, combined with latestDistinctList() to deduplicate resources by \ + keeping the latest version. Also tests ReconcileUtils methods for patching resources \ + with proper cache updates. + """) +class LatestDistinctIT { + + public static final String TEST_RESOURCE_NAME = "test-resource"; + public static final String CONFIG_MAP_1 = "config-map-1"; + public static final String DEFAULT_VALUE = "defaultValue"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(LatestDistinctTestReconciler.class) + .build(); + + @Test + void testLatestDistinctListWithTwoInformerEventSources() { + // Create the custom resource + var resource = createTestCustomResource(); + resource = extension.create(resource); + + // Create ConfigMaps with type1 label (watched by first event source) + var cm1 = createConfigMap(CONFIG_MAP_1, resource); + extension.create(cm1); + + // Wait for reconciliation + var reconciler = extension.getReconcilerOfType(LatestDistinctTestReconciler.class); + await() + .atMost(Duration.ofSeconds(5)) + .pollDelay(Duration.ofMillis(300)) + .untilAsserted( + () -> { + var updatedResource = + extension.get(LatestDistinctTestResource.class, TEST_RESOURCE_NAME); + assertThat(updatedResource.getStatus()).isNotNull(); + // Should see 3 distinct ConfigMaps + assertThat(updatedResource.getStatus().getConfigMapCount()).isEqualTo(1); + assertThat(reconciler.isErrorOccurred()).isFalse(); + // note that since there are two event source, and we do the update through one event + // source + // the other will still propagate an event + assertThat(reconciler.getNumberOfExecutions()).isEqualTo(2); + }); + } + + private LatestDistinctTestResource createTestCustomResource() { + var resource = new LatestDistinctTestResource(); + resource.setMetadata( + new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .withNamespace(extension.getNamespace()) + .build()); + resource.setSpec(new LatestDistinctTestResourceSpec()); + return resource; + } + + private ConfigMap createConfigMap(String name, LatestDistinctTestResource owner) { + Map labels = new HashMap<>(); + labels.put(LABEL_KEY, "val"); + + Map data = new HashMap<>(); + data.put("key", DEFAULT_VALUE); + + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(name) + .withNamespace(extension.getNamespace()) + .withLabels(labels) + .build()) + .withData(data) + .withNewMetadata() + .withName(name) + .withNamespace(extension.getNamespace()) + .withLabels(labels) + .addNewOwnerReference() + .withApiVersion(owner.getApiVersion()) + .withKind(owner.getKind()) + .withName(owner.getMetadata().getName()) + .withUid(owner.getMetadata().getUid()) + .endOwnerReference() + .endMetadata() + .build(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestReconciler.java new file mode 100644 index 0000000000..d182b52824 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestReconciler.java @@ -0,0 +1,147 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.baseapi.latestdistinct; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.ReconcileUtils; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +@ControllerConfiguration +public class LatestDistinctTestReconciler implements Reconciler { + + public static final String EVENT_SOURCE_1_NAME = "configmap-es-1"; + public static final String EVENT_SOURCE_2_NAME = "configmap-es-2"; + public static final String LABEL_KEY = "configmap-type"; + public static final String KEY_2 = "key2"; + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private volatile boolean errorOccurred = false; + + @Override + public UpdateControl reconcile( + LatestDistinctTestResource resource, Context context) { + + // Update status with information from ConfigMaps + if (resource.getStatus() == null) { + resource.setStatus(new LatestDistinctTestResourceStatus()); + } + var allConfigMaps = context.getSecondaryResourcesAsStream(ConfigMap.class).toList(); + if (allConfigMaps.size() < 2) { + // wait until both informers see the config map + return UpdateControl.noUpdate(); + } + // makes sure that distinc config maps returned + var distinctConfigMaps = + context + .getSecondaryResourcesAsStream(ConfigMap.class) + .collect(ReconcileUtils.latestDistinctList()); + if (distinctConfigMaps.size() != 1) { + errorOccurred = true; + throw new IllegalStateException(); + } + + resource.getStatus().setConfigMapCount(distinctConfigMaps.size()); + distinctConfigMaps.get(0).setData(Map.of(KEY_2, "val2")); + var updated = ReconcileUtils.update(context, distinctConfigMaps.get(0)); + + // makes sure that distinc config maps returned + distinctConfigMaps = + context + .getSecondaryResourcesAsStream(ConfigMap.class) + .collect(ReconcileUtils.latestDistinctList()); + + if (distinctConfigMaps.size() != 1) { + errorOccurred = true; + throw new IllegalStateException(); + } + if (!distinctConfigMaps.get(0).getData().containsKey(KEY_2) + || !distinctConfigMaps + .get(0) + .getMetadata() + .getResourceVersion() + .equals(updated.getMetadata().getResourceVersion())) { + errorOccurred = true; + throw new IllegalStateException(); + } + numberOfExecutions.incrementAndGet(); + return UpdateControl.patchStatus(resource); + } + + @Override + public List> prepareEventSources( + EventSourceContext context) { + var configEs1 = + InformerEventSourceConfiguration.from(ConfigMap.class, LatestDistinctTestResource.class) + .withName(EVENT_SOURCE_1_NAME) + .withLabelSelector(LABEL_KEY) + .withNamespacesInheritedFromController() + .withSecondaryToPrimaryMapper( + cm -> + Set.of( + new ResourceID( + cm.getMetadata().getOwnerReferences().get(0).getName(), + cm.getMetadata().getNamespace()))) + .build(); + + var configEs2 = + InformerEventSourceConfiguration.from(ConfigMap.class, LatestDistinctTestResource.class) + .withName(EVENT_SOURCE_2_NAME) + .withLabelSelector(LABEL_KEY) + .withNamespacesInheritedFromController() + .withSecondaryToPrimaryMapper( + cm -> + Set.of( + new ResourceID( + cm.getMetadata().getOwnerReferences().get(0).getName(), + cm.getMetadata().getNamespace()))) + .build(); + + return List.of( + new InformerEventSource<>(configEs1, context), + new InformerEventSource<>(configEs2, context)); + } + + @Override + public ErrorStatusUpdateControl updateErrorStatus( + LatestDistinctTestResource resource, + Context context, + Exception e) { + errorOccurred = true; + return ErrorStatusUpdateControl.noStatusUpdate(); + } + + public boolean isErrorOccurred() { + return errorOccurred; + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResource.java new file mode 100644 index 0000000000..546e349b0a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResource.java @@ -0,0 +1,40 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.baseapi.latestdistinct; + +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; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("ldt") +public class LatestDistinctTestResource + extends CustomResource + implements Namespaced { + + @Override + protected LatestDistinctTestResourceSpec initSpec() { + return new LatestDistinctTestResourceSpec(); + } + + @Override + protected LatestDistinctTestResourceStatus initStatus() { + return new LatestDistinctTestResourceStatus(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResourceSpec.java new file mode 100644 index 0000000000..acfefab85e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResourceSpec.java @@ -0,0 +1,28 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.baseapi.latestdistinct; + +public class LatestDistinctTestResourceSpec { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResourceStatus.java new file mode 100644 index 0000000000..fd5ff82df5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/latestdistinct/LatestDistinctTestResourceStatus.java @@ -0,0 +1,28 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.baseapi.latestdistinct; + +public class LatestDistinctTestResourceStatus { + private int configMapCount; + + public int getConfigMapCount() { + return configMapCount; + } + + public void setConfigMapCount(int configMapCount) { + this.configMapCount = configMapCount; + } +}