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 1204a3b844..2f7ca88941 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 @@ -32,10 +32,11 @@ public abstract class KubernetesDependentResource informerEventSource; + protected InformerEventSource informerEventSource; private boolean addOwnerReference; protected ResourceMatcher resourceMatcher; protected ResourceUpdatePreProcessor resourceUpdatePreProcessor; + protected TemporalResourceCache temporalResourceCache; @Override public void configureWith(KubernetesDependentResourceConfig config) { @@ -76,6 +77,8 @@ public void configureWith(ConfigurationService configurationService, this.informerEventSource = informerEventSource; this.addOwnerReference = addOwnerReference; initResourceMatcherAndUpdatePreProcessorIfNotSet(configurationService); + + temporalResourceCache = new TemporalResourceCache<>(informerEventSource); } protected void beforeCreate(R desired, P primary) { @@ -96,8 +99,10 @@ protected R create(R target, P primary, Context context) { "{}, with id: {}", target.getClass(), ResourceID.fromResource(target)); beforeCreate(target, primary); Class targetClass = (Class) target.getClass(); - return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) + var newResource = client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) .create(target); + temporalResourceCache.putAddedResource(newResource); + return newResource; } @SuppressWarnings("unchecked") @@ -107,10 +112,16 @@ protected R update(R actual, R target, P primary, Context context) { ResourceID.fromResource(target)); Class targetClass = (Class) target.getClass(); var updatedActual = resourceUpdatePreProcessor.replaceSpecOnActual(actual, target); - return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) - .replace(updatedActual); + R updatedResource = + client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) + .replace(updatedActual); + temporalResourceCache.putUpdatedResource(updatedResource, + actual.getMetadata().getResourceVersion()); + return updatedResource; } + + @Override public EventSource eventSource(EventSourceContext

context) { initResourceMatcherAndUpdatePreProcessorIfNotSet(context.getConfigurationService()); @@ -123,12 +134,6 @@ public EventSource eventSource(EventSourceContext

context) { return informerEventSource; } - public KubernetesDependentResource setInformerEventSource( - InformerEventSource informerEventSource) { - this.informerEventSource = informerEventSource; - return this; - } - @Override public void delete(P primary, Context context) { if (!addOwnerReference) { @@ -144,7 +149,16 @@ protected Class resourceType() { @Override public Optional getResource(P primaryResource) { - return informerEventSource.getAssociated(primaryResource); + var associatedSecondaryResourceIdentifier = + informerEventSource.getConfiguration().getAssociatedResourceIdentifier(); + var resourceId = + associatedSecondaryResourceIdentifier.associatedSecondaryID(primaryResource); + var tempCacheResource = temporalResourceCache.getResourceFromCache(resourceId); + if (tempCacheResource.isPresent()) { + return tempCacheResource; + } else { + return informerEventSource.get(resourceId); + } } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/TemporalResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/TemporalResourceCache.java new file mode 100644 index 0000000000..3c242005f9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/TemporalResourceCache.java @@ -0,0 +1,121 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +/** + *

+ * Temporal cache is used to solve the problem for {@link KubernetesDependentResource} that is, when + * a create or update is executed the subsequent getResource opeeration might not return the + * up-to-date resource from informer cache, since it is not received yet by webhook. + *

+ *

+ * The idea of the solution is, that since an update (for create is simpler) was done successfully, + * and optimistic locking is in place, there were no other operations between reading the resource + * from the cache and the actual update. So when the new resource is stored in the temporal cache + * only if the informer still has the previous resource version, from before the update. If not, + * that means there were already updates on the cache (either by the actual update from + * DependentResource or other) so the resource does not needs to be cached. Subsequently if event + * received from the informer, it means that the cache of the informer was updated, so it already + * contains a more fresh version of the resource. (See one caveat below) + *

+ * + * @param resource to cache. + */ +public class TemporalResourceCache implements ResourceEventHandler { + + private static final Logger log = LoggerFactory.getLogger(TemporalResourceCache.class); + + private final Map cache = new ConcurrentHashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + private final InformerEventSource informerEventSource; + + public TemporalResourceCache(InformerEventSource informerEventSource) { + this.informerEventSource = informerEventSource; + } + + @Override + public void onAdd(T t) { + removeResourceFromCache(t); + } + + /** + * In theory, it can happen that an older event is received, that is received before we updated + * the resource. So that is a situation still not covered, but it happens with extremely low + * probability and since it should trigger a new reconciliation, eventually the system is + * consistent. + * + * @param t old object + * @param t1 new object + */ + @Override + public void onUpdate(T t, T t1) { + removeResourceFromCache(t1); + } + + @Override + public void onDelete(T t, boolean b) { + removeResourceFromCache(t); + } + + private void removeResourceFromCache(T resource) { + lock.lock(); + try { + cache.remove(ResourceID.fromResource(resource)); + } finally { + lock.unlock(); + } + } + + public void putAddedResource(T newResource) { + lock.lock(); + try { + if (informerEventSource.get(ResourceID.fromResource(newResource)).isEmpty()) { + cache.put(ResourceID.fromResource(newResource), newResource); + } + } finally { + lock.unlock(); + } + } + + public void putUpdatedResource(T newResource, String previousResourceVersion) { + lock.lock(); + try { + var resourceId = ResourceID.fromResource(newResource); + var informerCacheResource = informerEventSource.get(resourceId); + if (informerCacheResource.isEmpty()) { + log.debug("No cached value present for resource: {}", newResource); + return; + } + // if this is not true that means the cache was already updated + if (informerCacheResource.get().getMetadata().getResourceVersion() + .equals(previousResourceVersion)) { + cache.put(resourceId, newResource); + } else { + // if something is in cache it's surely obsolete now + cache.remove(resourceId); + } + } finally { + lock.unlock(); + } + } + + public Optional getResourceFromCache(ResourceID resourceID) { + try { + lock.lock(); + return Optional.ofNullable(cache.get(resourceID)); + } finally { + lock.unlock(); + } + } +} 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 b29209f9e5..b1c535ac9e 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 @@ -104,4 +104,8 @@ public Optional getAssociated(P resource) { public Stream list(String namespace, Predicate predicate) { return manager().list(namespace, predicate); } + + public InformerConfiguration getConfiguration() { + return configuration; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index e65f34df48..0091ab0c4f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -139,6 +139,13 @@ public T remove(ResourceID key) { @Override public void put(ResourceID key, T resource) { getSource(key.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) - .ifPresent(c -> c.put(key, resource)); + .ifPresentOrElse(c -> c.put(key, resource), + () -> log.warn( + "Cannot put resource in the cache. No related cache found: {}. Resource: {}", + key, resource)); + } + + public void addEventHandler(ResourceEventHandler eventHandler) { + sources.values().forEach(i -> i.addEventHandler(eventHandler)); } } 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 c7de41f331..710f230580 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 @@ -81,4 +81,5 @@ public T remove(ResourceID key) { public void put(ResourceID key, T resource) { cache.put(key, resource); } + } 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 1dcfa2aa0e..a4f208690b 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 @@ -39,4 +39,5 @@ public void stop() { super.stop(); manager().stop(); } + } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java new file mode 100644 index 0000000000..e932419ec1 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java @@ -0,0 +1,93 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class KubernetesDependentResourceTest { + + private TemporalResourceCache temporalResourceCacheMock = mock(TemporalResourceCache.class); + private InformerEventSource informerEventSourceMock = mock(InformerEventSource.class); + private AssociatedSecondaryResourceIdentifier associatedResourceIdentifierMock = + mock(AssociatedSecondaryResourceIdentifier.class); + + KubernetesDependentResource kubernetesDependentResource = + new KubernetesDependentResource() { + { + this.temporalResourceCache = temporalResourceCacheMock; + this.informerEventSource = informerEventSourceMock; + } + + @Override + protected Object desired(HasMetadata primary, Context context) { + return testResource(); + } + }; + + @BeforeEach + public void setup() { + InformerConfiguration informerConfigurationMock = mock(InformerConfiguration.class); + when(informerEventSourceMock.getConfiguration()).thenReturn(informerConfigurationMock); + when(informerConfigurationMock.getAssociatedResourceIdentifier()) + .thenReturn(associatedResourceIdentifierMock); + when(associatedResourceIdentifierMock.associatedSecondaryID(any())) + .thenReturn(ResourceID.fromResource(testResource())); + } + + @Test + void getResourceCheckTheTemporalCacheFirst() { + when(temporalResourceCacheMock.getResourceFromCache(any())) + .thenReturn(Optional.of(testResource())); + + kubernetesDependentResource.getResource(primaryResource()); + + verify(temporalResourceCacheMock, times(1)).getResourceFromCache(any()); + verify(informerEventSourceMock, never()).get(any()); + } + + @Test + void getResourceGetsResourceFromInformerIfNotInTemporalCache() { + var resource = testResource(); + when(temporalResourceCacheMock.getResourceFromCache(any())).thenReturn(Optional.empty()); + when(informerEventSourceMock.get(any())).thenReturn(Optional.of(resource)); + + var res = kubernetesDependentResource.getResource(primaryResource()); + + verify(temporalResourceCacheMock, times(1)).getResourceFromCache(any()); + verify(informerEventSourceMock, times(1)).get(any()); + assertThat(res.orElseThrow()).isEqualTo(resource); + } + + TestCustomResource primaryResource() { + TestCustomResource testCustomResource = new TestCustomResource(); + testCustomResource.setMetadata(new ObjectMeta()); + testCustomResource.getMetadata().setName("test"); + testCustomResource.getMetadata().setNamespace("default"); + return testCustomResource; + } + + ConfigMap testResource() { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName("test"); + configMap.getMetadata().setNamespace("default"); + configMap.getMetadata().setResourceVersion("0"); + return configMap; + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/TemporalResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/TemporalResourceCacheTest.java new file mode 100644 index 0000000000..8196ac971d --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/TemporalResourceCacheTest.java @@ -0,0 +1,121 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TemporalResourceCacheTest { + + public static final String RESOURCE_VERSION = "1"; + private InformerEventSource informerEventSource = mock(InformerEventSource.class); + private TemporalResourceCache temporalResourceCache = + new TemporalResourceCache<>(informerEventSource); + + + @Test + void updateAddsTheResourceIntoCacheIfTheInformerHasThePreviousResourceVersion() { + var testResource = testResource(); + var prevTestResource = testResource(); + prevTestResource.getMetadata().setResourceVersion("0"); + when(informerEventSource.get(any())).thenReturn(Optional.of(prevTestResource)); + + temporalResourceCache.putUpdatedResource(testResource, "0"); + + var cached = temporalResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isPresent(); + } + + @Test + void updateNotAddsTheResourceIntoCacheIfTheInformerHasOtherVersion() { + var testResource = testResource(); + var informerCachedResource = testResource(); + informerCachedResource.getMetadata().setResourceVersion("x"); + when(informerEventSource.get(any())).thenReturn(Optional.of(informerCachedResource)); + + temporalResourceCache.putUpdatedResource(testResource, "0"); + + var cached = temporalResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isNotPresent(); + } + + @Test + void addOperationAddsTheResourceIfInformerCacheStillEmpty() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.empty()); + + temporalResourceCache.putAddedResource(testResource); + + var cached = temporalResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isPresent(); + } + + @Test + void addOperationNotAddsTheResourceIfInformerCacheNotEmpty() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.of(testResource())); + + temporalResourceCache.putAddedResource(testResource); + + var cached = temporalResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isNotPresent(); + } + + @Test + void onAddRemovesResourceFromCache() { + ConfigMap testResource = propagateTestResourceToCache(); + + temporalResourceCache.onAdd(testResource()); + + assertThat(temporalResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) + .isNotPresent(); + } + + @Test + void onUpdateRemovesResourceFromCache() { + ConfigMap testResource = propagateTestResourceToCache(); + + temporalResourceCache.onUpdate(testResource(), testResource()); + + assertThat(temporalResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) + .isNotPresent(); + } + + @Test + void onDeleteRemovesResourceFromCache() { + ConfigMap testResource = propagateTestResourceToCache(); + + temporalResourceCache.onDelete(testResource(), true); + + assertThat(temporalResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) + .isNotPresent(); + } + + private ConfigMap propagateTestResourceToCache() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.empty()); + temporalResourceCache.putAddedResource(testResource); + assertThat(temporalResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) + .isPresent(); + return testResource; + } + + ConfigMap testResource() { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName("test"); + configMap.getMetadata().setNamespace("default"); + configMap.getMetadata().setResourceVersion(RESOURCE_VERSION); + return configMap; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java similarity index 56% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index 20c0f20b80..d86385f1f1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.dependent; +package io.javaoperatorsdk.operator; import java.time.Duration; @@ -10,8 +10,10 @@ import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; +import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestReconciler; +import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class StandaloneDependentResourceIT { @@ -29,10 +31,38 @@ class StandaloneDependentResourceIT { void dependentResourceManagesDeployment() { StandaloneDependentTestCustomResource customResource = new StandaloneDependentTestCustomResource(); + customResource.setSpec(new StandaloneDependentTestCustomResourceSpec()); customResource.setMetadata(new ObjectMeta()); customResource.getMetadata().setName(DEPENDENT_TEST_NAME); var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + awaitForDeploymentReadyReplicas(1); + assertThat( + ((StandaloneDependentTestReconciler) operator.getFirstReconciler()).isErrorOccurred()) + .isFalse(); + } + + @Test + void executeUpdateForTestingCacheUpdateForGetResource() { + StandaloneDependentTestCustomResource customResource = + new StandaloneDependentTestCustomResource(); + customResource.setSpec(new StandaloneDependentTestCustomResourceSpec()); + customResource.setMetadata(new ObjectMeta()); + customResource.getMetadata().setName(DEPENDENT_TEST_NAME); + var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + + awaitForDeploymentReadyReplicas(1); + + createdCR.getSpec().setReplicaCount(2); + operator.replace(StandaloneDependentTestCustomResource.class, createdCR); + + awaitForDeploymentReadyReplicas(2); + assertThat( + ((StandaloneDependentTestReconciler) operator.getFirstReconciler()).isErrorOccurred()) + .isFalse(); + } + + void awaitForDeploymentReadyReplicas(int expectedReplicaCount) { await() .pollInterval(Duration.ofMillis(300)) .atMost(Duration.ofSeconds(50)) @@ -42,13 +72,13 @@ void dependentResourceManagesDeployment() { operator .getKubernetesClient() .resources(Deployment.class) - .inNamespace(createdCR.getMetadata().getNamespace()) + .inNamespace(operator.getNamespace()) .withName(DEPENDENT_TEST_NAME) .get(); return deployment != null && deployment.getStatus() != null && deployment.getStatus().getReadyReplicas() != null - && deployment.getStatus().getReadyReplicas() > 0; + && deployment.getStatus().getReadyReplicas() == expectedReplicaCount; }); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java index 3e6737c83c..e88d00c985 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java @@ -10,6 +10,7 @@ @Version("v1") @ShortNames("sdt") public class StandaloneDependentTestCustomResource - extends CustomResource + extends + CustomResource implements Namespaced { } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java new file mode 100644 index 0000000000..4a88cf6044 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +public class StandaloneDependentTestCustomResourceSpec { + + private int replicaCount; + + public StandaloneDependentTestCustomResourceSpec(int replicaCount) { + this.replicaCount = replicaCount; + } + + public StandaloneDependentTestCustomResourceSpec() { + this(1); + } + + public int getReplicaCount() { + return replicaCount; + } + + public StandaloneDependentTestCustomResourceSpec setReplicaCount(int replicaCount) { + this.replicaCount = replicaCount; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index d529f19c3d..08bc32f5e8 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.sample.standalonedependent; import java.util.List; +import java.util.Optional; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; @@ -16,33 +17,43 @@ public class StandaloneDependentTestReconciler implements Reconciler, EventSourceInitializer, - KubernetesClientAware { + KubernetesClientAware, ErrorStatusHandler { private KubernetesClient kubernetesClient; + private boolean errorOccurred = false; - KubernetesDependentResource configMapDependent; + KubernetesDependentResource deploymentDependent; public StandaloneDependentTestReconciler() { - configMapDependent = new DeploymentDependentResource(); + deploymentDependent = new DeploymentDependentResource(); } @Override public List prepareEventSources( EventSourceContext context) { - return List.of(configMapDependent.eventSource(context)); + return List.of(deploymentDependent.eventSource(context)); } @Override public UpdateControl reconcile( - StandaloneDependentTestCustomResource resource, Context context) { - configMapDependent.reconcile(resource, context); + StandaloneDependentTestCustomResource primary, Context context) { + deploymentDependent.reconcile(primary, context); + Optional deployment = deploymentDependent.getResource(primary); + if (deployment.isEmpty()) { + throw new IllegalStateException("Resource should not be empty after reconcile."); + } + + if (deployment.get().getSpec().getReplicas() != primary.getSpec().getReplicaCount()) { + // see https://github.com/java-operator-sdk/java-operator-sdk/issues/924 + throw new IllegalStateException("Something went wrong withe the cache mechanism."); + } return UpdateControl.noUpdate(); } @Override public void setKubernetesClient(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; - configMapDependent.setKubernetesClient(kubernetesClient); + deploymentDependent.setKubernetesClient(kubernetesClient); } @Override @@ -50,6 +61,17 @@ public KubernetesClient getKubernetesClient() { return this.kubernetesClient; } + @Override + public Optional updateErrorStatus( + StandaloneDependentTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + errorOccurred = true; + return Optional.empty(); + } + + public boolean isErrorOccurred() { + return errorOccurred; + } + private class DeploymentDependentResource extends KubernetesDependentResource { @@ -58,6 +80,7 @@ protected Deployment desired(StandaloneDependentTestCustomResource primary, Cont Deployment deployment = ReconcilerUtils.loadYaml(Deployment.class, getClass(), "nginx-deployment.yaml"); deployment.getMetadata().setName(primary.getMetadata().getName()); + deployment.getSpec().setReplicas(primary.getSpec().getReplicaCount()); deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); return deployment; } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index b3200903d1..fb9f90b6f2 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.sample; -import java.time.Duration; import java.util.*; import org.apache.commons.lang3.StringUtils; @@ -19,7 +18,6 @@ import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; -import static org.awaitility.Awaitility.await; @ControllerConfiguration(finalizerName = NO_FINALIZER) public class WebPageReconciler @@ -58,7 +56,6 @@ public UpdateControl reconcile(WebPage webPage, Context context) { WebPageStatus status = new WebPageStatus(); - waitUntilConfigMapAvailable(webPage); status.setHtmlConfigMap(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()); status.setAreWeGood("Yes!"); status.setErrorMessage(null); @@ -67,12 +64,6 @@ public UpdateControl reconcile(WebPage webPage, Context context) { return UpdateControl.updateStatus(webPage); } - // todo after implemented we can remove this method: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/924 - private void waitUntilConfigMapAvailable(WebPage webPage) { - await().atMost(Duration.ofSeconds(5)).until(() -> configMapDR.getResource(webPage).isPresent()); - } - @Override public Optional updateErrorStatus( WebPage resource, RetryInfo retryInfo, RuntimeException e) {