From ef1b414c7f4da424f7e1916078ac1d7482ebf43c Mon Sep 17 00:00:00 2001 From: Radovan Synek Date: Wed, 13 Apr 2022 09:53:02 +0200 Subject: [PATCH 0001/1176] fix: make webpage sample work OOTB (#1155) --- sample-operators/webpage/README.md | 3 +++ sample-operators/webpage/k8s/webpage.yaml | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sample-operators/webpage/README.md b/sample-operators/webpage/README.md index 8cc20a40f8..c591e712c3 100644 --- a/sample-operators/webpage/README.md +++ b/sample-operators/webpage/README.md @@ -46,6 +46,9 @@ page. Otherwise you can change the service to a LoadBalancer (e.g on a public cl You can also try to change the HTML code in `k8s/webpage.yaml` and do another `kubectl apply -f k8s/webpage.yaml`. This should update the actual NGINX deployment with the new configuration. +Note that there are multiple reconciler implementations that watch `WebPage` resources differentiated by a label. +When you create a new `WebPage` resource, make sure its label matches the active reconciler's label selector. + If you want the Operator to be running as a deployment in your cluster, follow the below steps. ### Build diff --git a/sample-operators/webpage/k8s/webpage.yaml b/sample-operators/webpage/k8s/webpage.yaml index e425b4750f..1aa41ff67a 100644 --- a/sample-operators/webpage/k8s/webpage.yaml +++ b/sample-operators/webpage/k8s/webpage.yaml @@ -1,8 +1,9 @@ apiVersion: "sample.javaoperatorsdk/v1" kind: WebPage metadata: - labels: - low-level: "true" +# Use labels to match the resource with different reconciler implementations: +# labels: +# low-level: "true" name: hellows spec: exposed: false From 71086d6ac3e56071b852f4cf143b2536573958e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Apr 2022 08:28:26 +0200 Subject: [PATCH 0002/1176] chore(deps): bump micrometer-core from 1.8.4 to 1.8.5 (#1159) Bumps [micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.8.4 to 1.8.5. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.8.4...v1.8.5) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8059cabdbb..df2502665d 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 3.22.0 4.2.0 2.6.6 - 1.8.4 + 1.8.5 2.11 3.10.1 From 794f9cf69cce1bbb8cb56c642c87859e73bb4974 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Apr 2022 07:56:39 +0200 Subject: [PATCH 0003/1176] chore(deps-dev): bump mockito-core from 4.4.0 to 4.5.0 (#1166) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.4.0...v4.5.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index df2502665d..0bd3c45fa0 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 5.12.2 1.7.36 2.17.2 - 4.4.0 + 4.5.0 3.12.0 1.0.1 0.19 From e971c4d9d7478117428f3503c31e5dd072548bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 20 Apr 2022 07:57:03 +0200 Subject: [PATCH 0004/1176] feat: remove primary to secondary mapper (handled automatically) (#1161) --- .../informer/InformerConfiguration.java | 38 ++----- .../operator/api/reconciler/Context.java | 3 + .../api/reconciler/DefaultContext.java | 26 ++++- .../processing/MultiResourceOwner.java | 23 ++++ ...actEventSourceHolderDependentResource.java | 2 +- .../KubernetesDependentResource.java | 8 +- .../processing/event/EventSourceManager.java | 5 + .../processing/event/EventSources.java | 14 ++- .../ExternalResourceCachingEventSource.java | 7 ++ .../event/source/CachingEventSource.java | 5 - .../source/PrimaryToSecondaryMapper.java | 9 -- .../source/SecondaryToPrimaryMapper.java | 4 +- .../source/informer/InformerEventSource.java | 68 +++++++---- .../informer/ManagedInformerEventSource.java | 15 --- .../informer/PrimaryToSecondaryIndex.java | 47 ++++++++ .../informer/InformerEventSourceTest.java | 4 +- .../informer/PrimaryToSecondaryIndexTest.java | 95 ++++++++++++++++ .../operator/junit/OperatorExtension.java | 2 +- .../operator/ControllerExecutionIT.java | 10 +- .../MultipleSecondaryEventSourceIT.java | 67 +++++++++++ ...CreateUpdateEventFilterTestReconciler.java | 2 +- ...formerEventSourceTestCustomReconciler.java | 2 +- ...pleSecondaryEventSourceCustomResource.java | 17 +++ ...ultipleSecondaryEventSourceReconciler.java | 106 ++++++++++++++++++ .../MultipleSecondaryEventSourceStatus.java | 5 + .../ConfigMapDependentResource1.java | 10 +- .../ConfigMapDependentResource2.java | 11 +- .../dependent/SecretDependentResource.java | 10 +- .../sample/DeploymentDependentResource.java | 8 +- .../operator/sample/WebappReconciler.java | 12 +- .../sample/ConfigMapDependentResource.java | 16 +-- .../sample/DeploymentDependentResource.java | 8 ++ .../sample/IngressDependentResource.java | 3 + .../sample/ServiceDependentResource.java | 4 + .../operator/sample/WebPageReconciler.java | 5 +- 35 files changed, 508 insertions(+), 163 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MultiResourceOwner.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndex.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndexTest.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleSecondaryEventSourceIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceStatus.java 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 55d52f42be..3ec615ffcb 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 @@ -8,32 +8,26 @@ import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @SuppressWarnings("rawtypes") -public interface InformerConfiguration +public interface InformerConfiguration extends ResourceConfiguration { - class DefaultInformerConfiguration extends - DefaultResourceConfiguration implements InformerConfiguration { + class DefaultInformerConfiguration extends + DefaultResourceConfiguration implements InformerConfiguration { private final SecondaryToPrimaryMapper secondaryToPrimaryMapper; - private final PrimaryToSecondaryMapper

primaryToSecondaryMapper; protected DefaultInformerConfiguration(String labelSelector, Class resourceClass, SecondaryToPrimaryMapper secondaryToPrimaryMapper, - PrimaryToSecondaryMapper

primaryToSecondaryMapper, Set namespaces) { super(labelSelector, resourceClass, namespaces); this.secondaryToPrimaryMapper = Objects.requireNonNullElse(secondaryToPrimaryMapper, Mappers.fromOwnerReference()); - this.primaryToSecondaryMapper = - Objects.requireNonNullElseGet(primaryToSecondaryMapper, () -> ResourceID::fromResource); } @@ -41,21 +35,14 @@ public SecondaryToPrimaryMapper getSecondaryToPrimaryMapper() { return secondaryToPrimaryMapper; } - public PrimaryToSecondaryMapper

getPrimaryToSecondaryMapper() { - return primaryToSecondaryMapper; - } - } SecondaryToPrimaryMapper getSecondaryToPrimaryMapper(); - PrimaryToSecondaryMapper

getPrimaryToSecondaryMapper(); - @SuppressWarnings("unused") class InformerConfigurationBuilder { - private SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; - private PrimaryToSecondaryMapper

associatedWith; + private SecondaryToPrimaryMapper secondaryToPrimaryMapper; private Set namespaces; private String labelSelector; private final Class resourceClass; @@ -66,17 +53,10 @@ private InformerConfigurationBuilder(Class resourceClass) { public InformerConfigurationBuilder withSecondaryToPrimaryMapper( SecondaryToPrimaryMapper secondaryToPrimaryMapper) { - this.secondaryToPrimaryResourcesIdSet = secondaryToPrimaryMapper; + this.secondaryToPrimaryMapper = secondaryToPrimaryMapper; return this; } - public InformerConfigurationBuilder withPrimaryToSecondaryMapper( - PrimaryToSecondaryMapper

associatedWith) { - this.associatedWith = associatedWith; - return this; - } - - public InformerConfigurationBuilder withNamespaces(String... namespaces) { this.namespaces = namespaces != null ? Set.of(namespaces) : Collections.emptySet(); return this; @@ -93,9 +73,9 @@ public InformerConfigurationBuilder withLabelSelector(String labelSelector return this; } - public InformerConfiguration build() { + public InformerConfiguration build() { return new DefaultInformerConfiguration<>(labelSelector, resourceClass, - secondaryToPrimaryResourcesIdSet, associatedWith, + secondaryToPrimaryMapper, namespaces); } } @@ -111,12 +91,10 @@ static InformerConfigurationBuilder from(Class resourceClass) { } static InformerConfigurationBuilder from( - InformerConfiguration configuration) { + InformerConfiguration configuration) { return new InformerConfigurationBuilder(configuration.getResourceClass()) .withNamespaces(configuration.getNamespaces()) .withLabelSelector(configuration.getLabelSelector()) - .withPrimaryToSecondaryMapper( - configuration.getPrimaryToSecondaryMapper()) .withSecondaryToPrimaryMapper(configuration.getSecondaryToPrimaryMapper()); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index b2c2270f0b..845810c8a1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.reconciler; import java.util.Optional; +import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -14,6 +15,8 @@ default Optional getSecondaryResource(Class expectedType) { return getSecondaryResource(expectedType, null); } + Set getSecondaryResources(Class expectedType); + Optional getSecondaryResource(Class expectedType, String eventSourceName); ControllerConfiguration

getControllerConfiguration(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 14fa516b53..00a94390c6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -1,11 +1,16 @@ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.MultiResourceOwner; public class DefaultContext

implements Context

{ @@ -28,9 +33,28 @@ public Optional getRetryInfo() { return Optional.ofNullable(retryInfo); } + @Override + @SuppressWarnings("unchecked") + public Set getSecondaryResources(Class expectedType) { + return controller.getEventSourceManager().getEventSourcesFor(expectedType).stream() + .map( + es -> { + if (es instanceof MultiResourceOwner) { + return ((MultiResourceOwner) es).getSecondaryResources(primaryResource); + } else { + return es.getSecondaryResource(primaryResource) + .map(List::of) + .orElse(Collections.emptyList()); + } + }) + .flatMap(List::stream) + .collect(Collectors.toSet()); + } + @Override public Optional getSecondaryResource(Class expectedType, String eventSourceName) { - return controller.getEventSourceManager() + return controller + .getEventSourceManager() .getResourceEventSourceFor(expectedType, eventSourceName) .getSecondaryResource(primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MultiResourceOwner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MultiResourceOwner.java new file mode 100644 index 0000000000..d3cc6b3770 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MultiResourceOwner.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.processing; + +import java.util.List; +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface MultiResourceOwner extends ResourceOwner { + + default Optional getSecondaryResource(P primary) { + var list = getSecondaryResources(primary); + if (list.isEmpty()) { + return Optional.empty(); + } else if (list.size() == 1) { + return Optional.of(list.get(0)); + } else { + throw new IllegalStateException("More than 1 secondary resource related to primary"); + } + + } + + List getSecondaryResources(P primary); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java index baf25aba81..f9dac7bc92 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java @@ -11,6 +11,7 @@ public abstract class AbstractEventSourceHolderDependentResource> extends AbstractDependentResource implements EventSourceProvider

{ + private T eventSource; private boolean isCacheFillerEventSource; @@ -48,7 +49,6 @@ protected void onUpdated(ResourceID primaryResourceId, R updated, R actual) { } } - @SuppressWarnings("unchecked") private RecentOperationCacheFiller recentOperationCacheFiller() { return (RecentOperationCacheFiller) eventSource; 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 abf6005597..b5dd2a720a 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 @@ -20,7 +20,6 @@ import io.javaoperatorsdk.operator.processing.dependent.Matcher; import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @@ -58,16 +57,11 @@ private void configureWith(String labelSelector, Set namespaces) { final var primaryResourcesRetriever = (this instanceof SecondaryToPrimaryMapper) ? (SecondaryToPrimaryMapper) this : Mappers.fromOwnerReference(); - final PrimaryToSecondaryMapper

secondaryResourceIdentifier = - (this instanceof PrimaryToSecondaryMapper) - ? (PrimaryToSecondaryMapper

) this - : ResourceID::fromResource; - InformerConfiguration ic = + InformerConfiguration ic = InformerConfiguration.from(resourceType()) .withLabelSelector(labelSelector) .withNamespaces(namespaces) .withSecondaryToPrimaryMapper(primaryResourcesRetriever) - .withPrimaryToSecondaryMapper(secondaryResourceIdentifier) .build(); configureWith(new InformerEventSource<>(ic, client)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java index da42a1e1e7..1ccc0ad2d9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; @@ -176,6 +177,10 @@ ResourceEventSource getResourceEventSourceFor( return getResourceEventSourceFor(dependentType, null); } + public List> getEventSourcesFor(Class dependentType) { + return eventSources.getEventSources(dependentType); + } + public ResourceEventSource getResourceEventSourceFor( Class dependentType, String qualifier) { Objects.requireNonNull(dependentType, "dependentType is Mandatory"); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java index 8ba4682da3..1d2d343831 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java @@ -1,10 +1,9 @@ package io.javaoperatorsdk.operator.processing.event; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.stream.Collectors; import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -131,4 +130,13 @@ private String keyAsString(Class dependentType, String name) { ? "(" + dependentType.getName() + ", " + name + ")" : dependentType.getName(); } + + @SuppressWarnings("unchecked") + public List> getEventSources(Class dependentType) { + final var sourcesForType = sources.get(keyFor(dependentType)); + return sourcesForType.values().stream() + .filter(ResourceEventSource.class::isInstance) + .map(es -> (ResourceEventSource) es) + .collect(Collectors.toList()); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java index f69b33dd43..b4bb0f7ef7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; @@ -50,4 +52,9 @@ public synchronized void handleRecentResourceUpdate(ResourceID resourceID, R res } }); } + + @Override + public Optional getSecondaryResource(P primary) { + return cache.get(ResourceID.fromResource(primary)); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index 2ddbf0d54c..8453651b6b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -51,9 +51,4 @@ public Optional getCachedValue(ResourceID resourceID) { return cache.get(resourceID); } - @Override - public Optional getSecondaryResource(P primary) { - return cache.get(ResourceID.fromResource(primary)); - } - } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java deleted file mode 100644 index f0ea122bda..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.ResourceID; - -@FunctionalInterface -public interface PrimaryToSecondaryMapper

{ - ResourceID toSecondaryResourceID(P primary); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java index 9f4db0c098..45573542c2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java @@ -5,6 +5,6 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; @FunctionalInterface -public interface SecondaryToPrimaryMapper { - Set toPrimaryResourceIDs(T dependentResource); +public interface SecondaryToPrimaryMapper { + Set toPrimaryResourceIDs(R dependentResource); } 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 b3b1637b94..08facecbb7 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 @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,6 +13,7 @@ import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationEventFilter; +import io.javaoperatorsdk.operator.processing.MultiResourceOwner; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -62,24 +65,30 @@ * @param

type of the primary resource */ public class InformerEventSource - extends ManagedInformerEventSource> - implements ResourceEventHandler, RecentOperationEventFilter { + extends ManagedInformerEventSource> + implements MultiResourceOwner, ResourceEventHandler, RecentOperationEventFilter { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); - private final InformerConfiguration configuration; + private final InformerConfiguration configuration; // always called from a synchronized method private final EventRecorder eventRecorder = new EventRecorder<>(); + // we need direct control for the indexer to propagate the just update resource also to the index + private final PrimaryToSecondaryIndex primaryToSecondaryIndex; public InformerEventSource( - InformerConfiguration configuration, EventSourceContext

context) { + InformerConfiguration configuration, EventSourceContext

context) { super(context.getClient().resources(configuration.getResourceClass()), configuration); this.configuration = configuration; + primaryToSecondaryIndex = + new PrimaryToSecondaryIndex<>(configuration.getSecondaryToPrimaryMapper()); } - public InformerEventSource(InformerConfiguration configuration, KubernetesClient client) { + public InformerEventSource(InformerConfiguration configuration, KubernetesClient client) { super(client.resources(configuration.getResourceClass()), configuration); this.configuration = configuration; + primaryToSecondaryIndex = + new PrimaryToSecondaryIndex<>(configuration.getSecondaryToPrimaryMapper()); } @Override @@ -87,6 +96,7 @@ public void onAdd(R resource) { if (log.isDebugEnabled()) { log.debug("On add event received for resource id: {}", ResourceID.fromResource(resource)); } + primaryToSecondaryIndex.onAddOrUpdate(resource); onAddOrUpdate("add", resource, () -> InformerEventSource.super.onAdd(resource)); } @@ -95,10 +105,21 @@ public void onUpdate(R oldObject, R newObject) { if (log.isDebugEnabled()) { log.debug("On update event received for resource id: {}", ResourceID.fromResource(newObject)); } + primaryToSecondaryIndex.onAddOrUpdate(newObject); onAddOrUpdate("update", newObject, () -> InformerEventSource.super.onUpdate(oldObject, newObject)); } + @Override + public void onDelete(R resource, boolean b) { + if (log.isDebugEnabled()) { + log.debug("On delete event received for resource id: {}", ResourceID.fromResource(resource)); + } + primaryToSecondaryIndex.onDelete(resource); + super.onDelete(resource, b); + propagateEvent(resource); + } + private synchronized void onAddOrUpdate(String operation, R newObject, Runnable superOnOp) { var resourceID = ResourceID.fromResource(newObject); if (eventRecorder.isRecordingFor(resourceID)) { @@ -106,7 +127,7 @@ private synchronized void onAddOrUpdate(String operation, R newObject, Runnable eventRecorder.recordEvent(newObject); return; } - if (temporalCacheHasResourceWithVersionAs(newObject)) { + if (temporaryCacheHasResourceWithSameVersionAs(newObject)) { log.debug( "Skipping event propagation for {}, since was a result of a reconcile action. Resource ID: {}", operation, @@ -122,13 +143,16 @@ private synchronized void onAddOrUpdate(String operation, R newObject, Runnable } } - @Override - public void onDelete(R resource, boolean b) { - if (log.isDebugEnabled()) { - log.debug("On delete event received for resource id: {}", ResourceID.fromResource(resource)); - } - super.onDelete(resource, b); - propagateEvent(resource); + private boolean temporaryCacheHasResourceWithSameVersionAs(R resource) { + var resourceID = ResourceID.fromResource(resource); + var res = temporaryResourceCache.getResourceFromCache(resourceID); + return res.map(r -> { + boolean resVersionsEqual = r.getMetadata().getResourceVersion() + .equals(resource.getMetadata().getResourceVersion()); + log.debug("Resource found in temporal cache for id: {} resource versions equal: {}", + resourceID, resVersionsEqual); + return resVersionsEqual; + }).orElse(false); } private void propagateEvent(R object) { @@ -152,20 +176,15 @@ private void propagateEvent(R object) { }); } - /** - * Retrieves the informed resource associated with the specified primary resource as defined by - * the function provided when this InformerEventSource was created - * - * @param resource the primary resource we want to retrieve the associated resource for - * @return the informed resource associated with the specified primary resource - */ @Override - public Optional getSecondaryResource(P resource) { - final var id = configuration.getPrimaryToSecondaryMapper().toSecondaryResourceID(resource); - return get(id); + public List getSecondaryResources(P primary) { + var secondaryIDs = + primaryToSecondaryIndex.getSecondaryResources(ResourceID.fromResource(primary)); + return secondaryIDs.stream().map(this::get).flatMap(Optional::stream) + .collect(Collectors.toList()); } - public InformerConfiguration getConfiguration() { + public InformerConfiguration getConfiguration() { return configuration; } @@ -183,6 +202,7 @@ public synchronized void handleRecentResourceCreate(ResourceID resourceID, R res } private void handleRecentCreateOrUpdate(R resource, Runnable runnable) { + primaryToSecondaryIndex.onAddOrUpdate(resource); if (eventRecorder.isRecordingFor(ResourceID.fromResource(resource))) { handleRecentResourceOperationAndStopEventRecording(resource); } else { 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 f7007072e9..606b0bc962 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 @@ -97,26 +97,11 @@ public Optional get(ResourceID resourceID) { } } - @Override - public abstract Optional getSecondaryResource(P primary); - @Override public Optional getCachedValue(ResourceID resourceID) { return get(resourceID); } - protected boolean temporalCacheHasResourceWithVersionAs(R resource) { - var resourceID = ResourceID.fromResource(resource); - var res = temporaryResourceCache.getResourceFromCache(resourceID); - return res.map(r -> { - boolean resVersionsEqual = r.getMetadata().getResourceVersion() - .equals(resource.getMetadata().getResourceVersion()); - log.debug("Resource found in temporal cache for id: {} resource versions equal: {}", - resourceID, resVersionsEqual); - return resVersionsEqual; - }).orElse(false); - } - @Override public Stream list(String namespace, Predicate predicate) { return manager().list(namespace, predicate); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndex.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndex.java new file mode 100644 index 0000000000..d83b60827b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndex.java @@ -0,0 +1,47 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.*; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; + +class PrimaryToSecondaryIndex { + + private SecondaryToPrimaryMapper secondaryToPrimaryMapper; + private Map> index = new HashMap<>(); + + public PrimaryToSecondaryIndex(SecondaryToPrimaryMapper secondaryToPrimaryMapper) { + this.secondaryToPrimaryMapper = secondaryToPrimaryMapper; + } + + public synchronized void onAddOrUpdate(R resource) { + Set primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource); + primaryResources.forEach( + primaryResource -> { + var resourceSet = index.computeIfAbsent(primaryResource, pr -> new HashSet<>()); + resourceSet.add(ResourceID.fromResource(resource)); + }); + } + + public synchronized void onDelete(R resource) { + Set primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource); + primaryResources.forEach( + primaryResource -> { + var secondaryResources = index.get(primaryResource); + secondaryResources.remove(ResourceID.fromResource(resource)); + if (secondaryResources.isEmpty()) { + index.remove(primaryResource); + } + }); + } + + public synchronized Set getSecondaryResources(ResourceID primary) { + var resourceIDs = index.get(primary); + if (resourceIDs == null) { + return Collections.emptySet(); + } else { + return Collections.unmodifiableSet(resourceIDs); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 96146789f6..7ac6779523 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -13,6 +13,7 @@ import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.fabric8.kubernetes.client.informers.cache.Indexer; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -38,7 +39,7 @@ class InformerEventSourceTest { mock(FilterWatchListMultiDeletable.class); private FilterWatchListDeletable labeledResourceClientMock = mock(FilterWatchListDeletable.class); private SharedIndexInformer informer = mock(SharedIndexInformer.class); - private InformerConfiguration informerConfiguration = + private InformerConfiguration informerConfiguration = mock(InformerConfiguration.class); @BeforeEach @@ -48,6 +49,7 @@ void setup() { when(specificResourceClientMock.withLabelSelector((String) null)) .thenReturn(labeledResourceClientMock); when(labeledResourceClientMock.runnableInformer(0)).thenReturn(informer); + when(informer.getIndexer()).thenReturn(mock(Indexer.class)); when(informerConfiguration.getSecondaryToPrimaryMapper()) .thenReturn(mock(SecondaryToPrimaryMapper.class)); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndexTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndexTest.java new file mode 100644 index 0000000000..ca73b135a7 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndexTest.java @@ -0,0 +1,95 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Set; + +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.ObjectMeta; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; + +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 PrimaryToSecondaryIndexTest { + + private SecondaryToPrimaryMapper secondaryToPrimaryMapperMock = + mock(SecondaryToPrimaryMapper.class); + private PrimaryToSecondaryIndex primaryToSecondaryIndex = + new PrimaryToSecondaryIndex<>(secondaryToPrimaryMapperMock); + + private ResourceID primaryID1 = new ResourceID("id1", "default"); + private ResourceID primaryID2 = new ResourceID("id2", "default"); + private ConfigMap secondary1 = secondary("secondary1"); + private ConfigMap secondary2 = secondary("secondary2"); + + @BeforeEach + void setup() { + when(secondaryToPrimaryMapperMock.toPrimaryResourceIDs(any())) + .thenReturn(Set.of(primaryID1, primaryID2)); + } + + @Test + void returnsEmptySetOnEmptyIndex() { + var res = primaryToSecondaryIndex.getSecondaryResources(ResourceID.fromResource(secondary1)); + assertThat(res).isEmpty(); + } + + @Test + void indexesNewResources() { + primaryToSecondaryIndex.onAddOrUpdate(secondary1); + + var secondaryResources1 = primaryToSecondaryIndex.getSecondaryResources(primaryID1); + var secondaryResources2 = primaryToSecondaryIndex.getSecondaryResources(primaryID2); + + assertThat(secondaryResources1).containsOnly(ResourceID.fromResource(secondary1)); + assertThat(secondaryResources2).containsOnly(ResourceID.fromResource(secondary1)); + } + + @Test + void indexesAdditionalResources() { + primaryToSecondaryIndex.onAddOrUpdate(secondary1); + primaryToSecondaryIndex.onAddOrUpdate(secondary2); + + var secondaryResources1 = primaryToSecondaryIndex.getSecondaryResources(primaryID1); + var secondaryResources2 = primaryToSecondaryIndex.getSecondaryResources(primaryID2); + + assertThat(secondaryResources1).containsOnly(ResourceID.fromResource(secondary1), + ResourceID.fromResource(secondary2)); + assertThat(secondaryResources2).containsOnly(ResourceID.fromResource(secondary1), + ResourceID.fromResource(secondary2)); + } + + @Test + void removingResourceFromIndex() { + primaryToSecondaryIndex.onAddOrUpdate(secondary1); + primaryToSecondaryIndex.onAddOrUpdate(secondary2); + primaryToSecondaryIndex.onDelete(secondary1); + + var secondaryResources1 = primaryToSecondaryIndex.getSecondaryResources(primaryID1); + var secondaryResources2 = primaryToSecondaryIndex.getSecondaryResources(primaryID2); + + assertThat(secondaryResources1).containsOnly(ResourceID.fromResource(secondary2)); + assertThat(secondaryResources2).containsOnly(ResourceID.fromResource(secondary2)); + + primaryToSecondaryIndex.onDelete(secondary2); + + secondaryResources1 = primaryToSecondaryIndex.getSecondaryResources(primaryID1); + secondaryResources2 = primaryToSecondaryIndex.getSecondaryResources(primaryID2); + + assertThat(secondaryResources1).isEmpty(); + assertThat(secondaryResources2).isEmpty(); + } + + ConfigMap secondary(String name) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName(name); + configMap.getMetadata().setNamespace("default"); + return configMap; + } +} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 7602d40295..d95e9466a7 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -76,7 +76,7 @@ public Reconciler getFirstReconciler() { return reconcilers().findFirst().orElseThrow(); } - public T getControllerOfType(Class type) { + public T getReconcilerOfType(Class type) { return reconcilers() .filter(type::isInstance) .map(type::cast) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index 67bc908482..7aeae40622 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -23,7 +23,7 @@ class ControllerExecutionIT { @Test void configMapGetsCreatedForTestCustomResource() { - operator.getControllerOfType(TestReconciler.class).setUpdateStatus(true); + operator.getReconcilerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); operator.create(TestCustomResource.class, resource); @@ -35,8 +35,8 @@ void configMapGetsCreatedForTestCustomResource() { @Test void patchesStatusForTestCustomResource() { - operator.getControllerOfType(TestReconciler.class).setPatchStatus(true); - operator.getControllerOfType(TestReconciler.class).setUpdateStatus(true); + operator.getReconcilerOfType(TestReconciler.class).setPatchStatus(true); + operator.getReconcilerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); operator.create(TestCustomResource.class, resource); @@ -46,7 +46,7 @@ void patchesStatusForTestCustomResource() { @Test void eventIsSkippedChangedOnMetadataOnlyUpdate() { - operator.getControllerOfType(TestReconciler.class).setUpdateStatus(false); + operator.getReconcilerOfType(TestReconciler.class).setUpdateStatus(false); TestCustomResource resource = TestUtils.testCustomResource(); operator.create(TestCustomResource.class, resource); @@ -57,7 +57,7 @@ void eventIsSkippedChangedOnMetadataOnlyUpdate() { @Test void cleanupExecuted() { - operator.getControllerOfType(TestReconciler.class).setUpdateStatus(true); + operator.getReconcilerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); resource = operator.create(TestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleSecondaryEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleSecondaryEventSourceIT.java new file mode 100644 index 0000000000..9168b47164 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleSecondaryEventSourceIT.java @@ -0,0 +1,67 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; + +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.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.multiplesecondaryeventsource.MultipleSecondaryEventSourceCustomResource; +import io.javaoperatorsdk.operator.sample.multiplesecondaryeventsource.MultipleSecondaryEventSourceReconciler; + +import static org.awaitility.Awaitility.await; + +class MultipleSecondaryEventSourceIT { + + public static final String TEST_RESOURCE_NAME = "testresource"; + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder().withReconciler(MultipleSecondaryEventSourceReconciler.class) + .build(); + + @Test + void receivingPeriodicEvents() { + MultipleSecondaryEventSourceCustomResource resource = createTestCustomResource(); + + operator.create(MultipleSecondaryEventSourceCustomResource.class, resource); + + var reconciler = operator.getReconcilerOfType(MultipleSecondaryEventSourceReconciler.class); + + await().pollDelay(Duration.ofMillis(300)) + .until(() -> reconciler.getNumberOfExecutions() <= 3); + + int numberOfInitialExecutions = reconciler.getNumberOfExecutions(); + + updateConfigMap(resource, 1); + + await().pollDelay(Duration.ofMillis(300)) + .until(() -> reconciler.getNumberOfExecutions() == numberOfInitialExecutions + 1); + + updateConfigMap(resource, 2); + + await().pollDelay(Duration.ofMillis(300)) + .until(() -> reconciler.getNumberOfExecutions() == numberOfInitialExecutions + 2); + } + + private void updateConfigMap(MultipleSecondaryEventSourceCustomResource resource, int number) { + ConfigMap map1 = operator.get(ConfigMap.class, + number == 1 ? MultipleSecondaryEventSourceReconciler.getName1(resource) + : MultipleSecondaryEventSourceReconciler.getName2(resource)); + map1.getData().put("value2", "value2"); + operator.replace(ConfigMap.class, map1); + } + + public MultipleSecondaryEventSourceCustomResource createTestCustomResource() { + MultipleSecondaryEventSourceCustomResource resource = + new MultipleSecondaryEventSourceCustomResource(); + resource.setMetadata( + new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .withNamespace(operator.getNamespace()) + .build()); + return resource; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index aa50e5fa4f..acfc109f72 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -101,7 +101,7 @@ private ConfigMap createConfigMap(CreateUpdateEventFilterTestCustomResource reso @Override public Map prepareEventSources( EventSourceContext context) { - InformerConfiguration informerConfiguration = + InformerConfiguration informerConfiguration = InformerConfiguration.from(context, ConfigMap.class) .withLabelSelector("integrationtest = " + this.getClass().getSimpleName()) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index ebcbf85994..a4aa0f4ddd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -41,7 +41,7 @@ public class InformerEventSourceTestCustomReconciler public Map prepareEventSources( EventSourceContext context) { - InformerConfiguration config = + InformerConfiguration config = InformerConfiguration.from(context, ConfigMap.class) .withSecondaryToPrimaryMapper(Mappers.fromAnnotation(RELATED_RESOURCE_NAME)) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceCustomResource.java new file mode 100644 index 0000000000..95330ef8b3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.multiplesecondaryeventsource; + +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.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("MaxIntervalTestCustomResource") +@ShortNames("mit") +public class MultipleSecondaryEventSourceCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceReconciler.java new file mode 100644 index 0000000000..ce27f0f363 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceReconciler.java @@ -0,0 +1,106 @@ +package io.javaoperatorsdk.operator.sample.multiplesecondaryeventsource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration +public class MultipleSecondaryEventSourceReconciler + implements Reconciler, TestExecutionInfoProvider, + EventSourceInitializer, KubernetesClientAware { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private KubernetesClient client; + + @Override + public UpdateControl reconcile( + MultipleSecondaryEventSourceCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + + if (client.configMaps().inNamespace(resource.getMetadata().getNamespace()) + .withName(getName1(resource)).get() == null) { + client.configMaps().inNamespace(resource.getMetadata().getNamespace()) + .createOrReplace(configMap(getName1(resource), resource)); + } + if (client.configMaps().inNamespace(resource.getMetadata().getNamespace()) + .withName(getName2(resource)).get() == null) { + client.configMaps().inNamespace(resource.getMetadata().getNamespace()) + .createOrReplace(configMap(getName2(resource), resource)); + } + + if (numberOfExecutions.get() >= 3) { + if (context.getSecondaryResources(ConfigMap.class).size() != 2) { + throw new IllegalStateException("There should be 2 related config maps"); + } + } + return UpdateControl.noUpdate(); + } + + public static String getName1(MultipleSecondaryEventSourceCustomResource resource) { + return resource.getMetadata().getName() + "1"; + } + + public static String getName2(MultipleSecondaryEventSourceCustomResource resource) { + return resource.getMetadata().getName() + "2"; + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + + + var config = InformerConfiguration.from(context, ConfigMap.class) + .withNamespaces(context.getControllerConfiguration().getNamespaces()) + .withLabelSelector("multisecondary") + .withSecondaryToPrimaryMapper(s -> { + var name = + s.getMetadata().getName().subSequence(0, s.getMetadata().getName().length() - 1); + return Set.of(new ResourceID(name.toString(), s.getMetadata().getNamespace())); + }).build(); + InformerEventSource configMapEventSource = + new InformerEventSource(config, + context); + return EventSourceInitializer.nameEventSources(configMapEventSource); + } + + ConfigMap configMap(String name, MultipleSecondaryEventSourceCustomResource resource) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName(name); + configMap.getMetadata().setNamespace(resource.getMetadata().getNamespace()); + configMap.setData(new HashMap<>()); + configMap.getData().put(name, name); + HashMap labels = new HashMap<>(); + labels.put("multisecondary", "true"); + configMap.getMetadata().setLabels(labels); + configMap.addOwnerReference(resource); + return configMap; + } + + @Override + public KubernetesClient getKubernetesClient() { + return client; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.client = kubernetesClient; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceStatus.java new file mode 100644 index 0000000000..2a78bf0531 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplesecondaryeventsource/MultipleSecondaryEventSourceStatus.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.sample.multiplesecondaryeventsource; + +public class MultipleSecondaryEventSourceStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource1.java index c0469c6a7c..19fd28b631 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource1.java @@ -9,13 +9,10 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; @KubernetesDependent(labelSelector = "dependent = cm1") public class ConfigMapDependentResource1 extends - CRUKubernetesDependentResource - implements PrimaryToSecondaryMapper { + CRUKubernetesDependentResource { public ConfigMapDependentResource1() { super(ConfigMap.class); @@ -45,9 +42,4 @@ protected ConfigMap desired(OrderedManagedDependentCustomResource primary, return configMap; } - @Override - public ResourceID toSecondaryResourceID(OrderedManagedDependentCustomResource primary) { - return new ResourceID(primary.getMetadata().getName() + "1", - primary.getMetadata().getNamespace()); - } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource2.java index 40e55d4589..2bffdfa8c1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource2.java @@ -9,13 +9,10 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; @KubernetesDependent(labelSelector = "dependent = cm2") public class ConfigMapDependentResource2 extends - CRUKubernetesDependentResource - implements PrimaryToSecondaryMapper { + CRUKubernetesDependentResource { public ConfigMapDependentResource2() { super(ConfigMap.class); @@ -45,10 +42,4 @@ protected ConfigMap desired(OrderedManagedDependentCustomResource primary, return configMap; } - @Override - public ResourceID toSecondaryResourceID(OrderedManagedDependentCustomResource primary) { - return new ResourceID(primary.getMetadata().getName() + "2", - primary.getMetadata().getNamespace()); - } - } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 7f39d09130..b1c516df8e 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -10,12 +10,10 @@ import io.javaoperatorsdk.operator.processing.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.sample.MySQLSchema; public class SecretDependentResource extends KubernetesDependentResource - implements PrimaryToSecondaryMapper, Creator { + implements Creator { public static final String SECRET_FORMAT = "%s-secret"; public static final String USERNAME_FORMAT = "%s-user"; @@ -57,10 +55,4 @@ public Result match(Secret actual, MySQLSchema primary, Context - implements Creator, Updater { +public class DeploymentDependentResource + extends CRUKubernetesDependentResource { public DeploymentDependentResource() { super(Deployment.class); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index f317d17f91..697f72cfdd 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -29,7 +29,6 @@ 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.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; @@ -59,18 +58,9 @@ public Map prepareEventSources(EventSourceContext c .map(ResourceID::fromResource) .collect(Collectors.toSet()); - /* - * We retrieve the Tomcat instance associated with out Webapp from its spec - */ - final PrimaryToSecondaryMapper tomcatFromWebAppSpec = - (Webapp webapp) -> new ResourceID( - webapp.getSpec().getTomcat(), - webapp.getMetadata().getNamespace()); - - InformerConfiguration configuration = + InformerConfiguration configuration = InformerConfiguration.from(context, Tomcat.class) .withSecondaryToPrimaryMapper(webappsMatchingTomcatName) - .withPrimaryToSecondaryMapper(tomcatFromWebAppSpec) .build(); return EventSourceInitializer .nameEventSources(new InformerEventSource<>(configuration, context)); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java index 35ceda38c7..bba817436c 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java @@ -12,16 +12,14 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import static io.javaoperatorsdk.operator.sample.Utils.configMapName; import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; // this annotation only activates when using managed dependents and is not otherwise needed -@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) -class ConfigMapDependentResource extends CRUKubernetesDependentResource - implements PrimaryToSecondaryMapper { +@KubernetesDependent(labelSelector = SELECTOR) +class ConfigMapDependentResource extends CRUKubernetesDependentResource { private static final Logger log = LoggerFactory.getLogger(ConfigMapDependentResource.class); @@ -33,11 +31,14 @@ public ConfigMapDependentResource() { protected ConfigMap desired(WebPage webPage, Context context) { Map data = new HashMap<>(); data.put("index.html", webPage.getSpec().getHtml()); + Map labels = new HashMap<>(); + labels.put(SELECTOR, "true"); return new ConfigMapBuilder() .withMetadata( new ObjectMetaBuilder() .withName(configMapName(webPage)) .withNamespace(webPage.getMetadata().getNamespace()) + .withLabels(labels) .build()) .withData(data) .build(); @@ -57,9 +58,4 @@ public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, .delete(); return res; } - - @Override - public ResourceID toSecondaryResourceID(WebPage primary) { - return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); - } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index ee6322e058..80a41e24d8 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -1,5 +1,8 @@ package io.javaoperatorsdk.operator.sample; +import java.util.HashMap; +import java.util.Map; + import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -9,6 +12,7 @@ import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.sample.Utils.configMapName; import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; // this annotation only activates when using managed dependents and is not otherwise needed @KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) @@ -20,10 +24,13 @@ public DeploymentDependentResource() { @Override protected Deployment desired(WebPage webPage, Context context) { + Map labels = new HashMap<>(); + labels.put(SELECTOR, "true"); var deploymentName = deploymentName(webPage); Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); deployment.getMetadata().setName(deploymentName); deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getMetadata().setLabels(labels); deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); deployment @@ -40,6 +47,7 @@ protected Deployment desired(WebPage webPage, Context context) { .get(0) .setConfigMap( new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/IngressDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/IngressDependentResource.java index a6de51919a..074f36cffb 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/IngressDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/IngressDependentResource.java @@ -3,9 +3,12 @@ import io.fabric8.kubernetes.api.model.networking.v1.Ingress; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import static io.javaoperatorsdk.operator.sample.Utils.*; +// this annotation only activates when using managed dependents and is not otherwise needed +@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) public class IngressDependentResource extends CRUKubernetesDependentResource { public IngressDependentResource() { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index b501fdb405..84d670cfc5 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -11,6 +11,7 @@ import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; import static io.javaoperatorsdk.operator.sample.Utils.serviceName; +import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; // this annotation only activates when using managed dependents and is not otherwise needed @KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) @@ -22,9 +23,12 @@ public ServiceDependentResource() { @Override protected Service desired(WebPage webPage, Context context) { + Map serviceLabels = new HashMap<>(); + serviceLabels.put(SELECTOR, "true"); Service service = loadYaml(Service.class, getClass(), "service.yaml"); service.getMetadata().setName(serviceName(webPage)); service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + service.getMetadata().setLabels(serviceLabels); Map labels = new HashMap<>(); labels.put("app", deploymentName(webPage)); service.getSpec().setSelector(labels); 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 7f8f38afb8..5bccefb56e 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 @@ -66,8 +66,7 @@ public Map prepareEventSources(EventSourceContext new InformerEventSource<>(InformerConfiguration.from(context, Ingress.class) .withLabelSelector(LOW_LEVEL_LABEL_KEY) .build(), context); - return EventSourceInitializer.nameEventSources(configMapEventSource, - deploymentEventSource, + return EventSourceInitializer.nameEventSources(configMapEventSource, deploymentEventSource, serviceEventSource, ingressEventSource); } @@ -208,7 +207,7 @@ private Deployment makeDesiredDeployment(WebPage webPage, String deploymentName, private ConfigMap makeDesiredHtmlConfigMap(String ns, String configMapName, WebPage webPage) { Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); + data.put(INDEX_HTML, webPage.getSpec().getHtml()); ConfigMap configMap = new ConfigMapBuilder() .withMetadata( From c5f877b957fd704094070c44c0d2754cf72142d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 07:35:15 +0200 Subject: [PATCH 0005/1176] chore(deps): bump maven-javadoc-plugin from 3.3.2 to 3.4.0 (#1167) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0bd3c45fa0..79ea87e1ad 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ 2.11 3.10.1 3.0.0-M5 - 3.3.2 + 3.4.0 3.2.0 3.2.1 3.2.2 From 6bcc9c95023763a2703980c3f7d44cbc9d77d5c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Apr 2022 08:37:59 +0200 Subject: [PATCH 0006/1176] chore(deps): bump nexus-staging-maven-plugin from 1.6.12 to 1.6.13 (#1173) Bumps nexus-staging-maven-plugin from 1.6.12 to 1.6.13. --- updated-dependencies: - dependency-name: org.sonatype.plugins:nexus-staging-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 79ea87e1ad..c6a35c8c36 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 3.2.2 3.2.0 3.0.1 - 1.6.12 + 1.6.13 2.8.2 2.5.2 5.0.0 From 0c7d5c989787e7d2bd3bce463002a9d550574e62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Apr 2022 08:38:15 +0200 Subject: [PATCH 0007/1176] chore(deps-dev): bump mockito-core from 4.5.0 to 4.5.1 (#1174) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.5.0 to 4.5.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.5.0...v4.5.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c6a35c8c36..40b1a3439f 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 5.12.2 1.7.36 2.17.2 - 4.5.0 + 4.5.1 3.12.0 1.0.1 0.19 From d9ebacef4de2cb316f45fbb557e1d29a84d35f4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Apr 2022 08:38:35 +0200 Subject: [PATCH 0008/1176] chore(deps): bump spring-boot.version from 2.6.6 to 2.6.7 (#1172) Bumps `spring-boot.version` from 2.6.6 to 2.6.7. Updates `spring-boot-dependencies` from 2.6.6 to 2.6.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.6...v2.6.7) Updates `spring-boot-maven-plugin` from 2.6.6 to 2.6.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.6...v2.6.7) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 40b1a3439f..9b5d574e18 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ 1.13.0 3.22.0 4.2.0 - 2.6.6 + 2.6.7 1.8.5 2.11 From dca90ed3c8a6afe0f226cf17e8e6f50046d57d3a Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 22 Apr 2022 16:01:55 +0200 Subject: [PATCH 0009/1176] feat: make default watched namespaces behavior explicit (#1177) * feat: make default watched namespaces behavior explicit This uses the constant that was initially introduced for the same reason on KubernetesDependent, which is now moved to Constants. Uncovered several issues with configuration overriding in the process and added test coverage. Fixes #1176 * refactor: DEPLOYED_NAMESPACE_ONLY -> CURRENT_NAMESPACE_ONLY --- .../AnnotationControllerConfiguration.java | 3 +- .../ControllerConfigurationOverrider.java | 14 +++- .../config/DefaultResourceConfiguration.java | 15 ++-- .../api/config/ResourceConfiguration.java | 35 ++++++--- .../operator/api/reconciler/Constants.java | 1 + .../reconciler/ControllerConfiguration.java | 2 +- .../kubernetes/KubernetesDependent.java | 1 - .../KubernetesDependentResourceConfig.java | 6 +- .../operator/OperatorTest.java | 8 +-- .../ControllerConfigurationOverriderTest.java | 69 ++++++++++++++++-- .../config/MockControllerConfiguration.java | 18 +++++ .../api/config/ResourceConfigurationTest.java | 71 +++++++++++++++++++ .../operator/processing/ControllerTest.java | 16 ++--- .../event/EventSourceManagerTest.java | 5 +- .../event/ReconciliationDispatcherTest.java | 63 ++++++++-------- .../informer/InformerEventSourceTest.java | 21 +++--- 16 files changed, 256 insertions(+), 92 deletions(-) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ResourceConfigurationTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index 19535c6d3e..ddd69dff53 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -66,7 +66,8 @@ public boolean isGenerationAware() { @Override public Set getNamespaces() { - return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, new String[] {})); + return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, + new String[] {Constants.WATCH_ALL_NAMESPACES})); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index 57b8640a8b..f045a4a2e3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -19,7 +19,7 @@ public class ControllerConfigurationOverrider { private String finalizer; private boolean generationAware; - private final Set namespaces; + private Set namespaces; private RetryConfiguration retry; private String labelSelector; private ResourceEventFilter customResourcePredicate; @@ -52,8 +52,8 @@ public ControllerConfigurationOverrider withGenerationAware(boolean generatio return this; } - public ControllerConfigurationOverrider withCurrentNamespace() { - namespaces.clear(); + public ControllerConfigurationOverrider watchingOnlyCurrentNamespace() { + this.namespaces = ResourceConfiguration.CURRENT_NAMESPACE_ONLY; return this; } @@ -64,6 +64,9 @@ public ControllerConfigurationOverrider addingNamespaces(String... namespaces public ControllerConfigurationOverrider removingNamespaces(String... namespaces) { List.of(namespaces).forEach(this.namespaces::remove); + if (this.namespaces.isEmpty()) { + this.namespaces = ResourceConfiguration.DEFAULT_NAMESPACES; + } return this; } @@ -73,6 +76,11 @@ public ControllerConfigurationOverrider settingNamespace(String namespace) { return this; } + public ControllerConfigurationOverrider watchingAllNamespaces() { + this.namespaces = ResourceConfiguration.DEFAULT_NAMESPACES; + return this; + } + public ControllerConfigurationOverrider withRetry(RetryConfiguration retry) { this.retry = retry; return this; 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 4959b827ad..7ee116ac60 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 @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.api.config; -import java.util.Collections; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -10,21 +9,22 @@ public class DefaultResourceConfiguration private final String labelSelector; private final Set namespaces; - private final boolean watchAllNamespaces; private final Class resourceClass; public DefaultResourceConfiguration(String labelSelector, Class resourceClass, String... namespaces) { this(labelSelector, resourceClass, - namespaces != null ? Set.of(namespaces) : Collections.emptySet()); + namespaces == null || namespaces.length == 0 ? ResourceConfiguration.DEFAULT_NAMESPACES + : Set.of(namespaces)); } public DefaultResourceConfiguration(String labelSelector, Class resourceClass, Set namespaces) { this.labelSelector = labelSelector; this.resourceClass = resourceClass; - this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); - this.watchAllNamespaces = this.namespaces.isEmpty(); + this.namespaces = + namespaces == null || namespaces.isEmpty() ? ResourceConfiguration.DEFAULT_NAMESPACES + : namespaces; } @Override @@ -42,11 +42,6 @@ public Set getNamespaces() { return namespaces; } - @Override - public boolean watchAllNamespaces() { - return watchAllNamespaces; - } - @Override public Class getResourceClass() { return resourceClass; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java index 2d947050e7..fe85476735 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java @@ -10,15 +10,18 @@ public interface ResourceConfiguration { + Set DEFAULT_NAMESPACES = Set.of(Constants.WATCH_ALL_NAMESPACES); + Set CURRENT_NAMESPACE_ONLY = Set.of(Constants.WATCH_CURRENT_NAMESPACE); + default String getResourceTypeName() { return ReconcilerUtils.getResourceTypeName(getResourceClass()); } /** * Retrieves the label selector that is used to filter which resources are actually watched by the - * associated event source. See - * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on - * syntax. + * associated event source. See the official documentation on the + * topic + * for more details on syntax. * * @return the label selector filtering watched resources */ @@ -32,7 +35,7 @@ default Class getResourceClass() { } default Set getNamespaces() { - return Collections.emptySet(); + return DEFAULT_NAMESPACES; } default boolean watchAllNamespaces() { @@ -40,7 +43,8 @@ default boolean watchAllNamespaces() { } static boolean allNamespacesWatched(Set namespaces) { - return namespaces == null || namespaces.isEmpty(); + failIfNotValid(namespaces); + return DEFAULT_NAMESPACES.equals(namespaces); } default boolean watchCurrentNamespace() { @@ -48,9 +52,24 @@ default boolean watchCurrentNamespace() { } static boolean currentNamespaceWatched(Set namespaces) { - return namespaces != null - && namespaces.size() == 1 - && namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE); + failIfNotValid(namespaces); + return CURRENT_NAMESPACE_ONLY.equals(namespaces); + } + + static void failIfNotValid(Set namespaces) { + if (namespaces != null && !namespaces.isEmpty()) { + final var present = namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE) + || namespaces.contains(Constants.WATCH_ALL_NAMESPACES); + if (!present || namespaces.size() == 1) { + return; + } + } + + throw new IllegalArgumentException( + "Must specify namespaces. To watch all namespaces, use only '" + + Constants.WATCH_ALL_NAMESPACES + + "'. To watch only the namespace in which the operator is deployed, use only '" + + Constants.WATCH_CURRENT_NAMESPACE + "'"); } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java index 285bcb16cb..7d7d44ec32 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -4,6 +4,7 @@ public final class Constants { public static final String NO_VALUE_SET = ""; public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; + public static final String WATCH_ALL_NAMESPACES = "JOSDK_ALL_NAMESPACES"; public static final long NO_RECONCILIATION_MAX_INTERVAL = -1L; private Constants() {} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index 3bd5b93a61..a5e1459cb7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -39,7 +39,7 @@ * * @return the list of namespaces this controller monitors */ - String[] namespaces() default {}; + String[] namespaces() default Constants.WATCH_ALL_NAMESPACES; /** * Optional label selector used to identify the set of custom resources the controller will acc diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java index fbebe34648..8ce04d752e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java @@ -11,7 +11,6 @@ @Target({ElementType.TYPE}) public @interface KubernetesDependent { String SAME_AS_PARENT = "JOSDK_SAME_AS_PARENT"; - String WATCH_ALL_NAMESPACES = "JOSDK_ALL_NAMESPACES"; String[] DEFAULT_NAMESPACES = {SAME_AS_PARENT}; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java index 48a93cb102..5f325bbfcc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java @@ -37,11 +37,7 @@ public KubernetesDependentResourceConfig setLabelSelector(String labelSelector) } public Set namespaces() { - if (!namespaces.contains(KubernetesDependent.WATCH_ALL_NAMESPACES)) { - return namespaces; - } else { - return Collections.emptySet(); - } + return namespaces; } public String labelSelector() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index f4de6b2141..2a1b48f019 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -10,20 +10,17 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.AbstractConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; @SuppressWarnings("rawtypes") class OperatorTest { private final KubernetesClient kubernetesClient = MockKubernetesClient.client(ConfigMap.class); - private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); private final Operator operator = new Operator(kubernetesClient); private final FooReconciler fooReconciler = new FooReconciler(); @@ -35,10 +32,9 @@ static void setUpConfigurationServiceProvider() { @Test @DisplayName("should register `Reconciler` to Controller") - @SuppressWarnings("unchecked") public void shouldRegisterReconcilerToController() { // given - when(configuration.getResourceClass()).thenReturn(ConfigMap.class); + final var configuration = MockControllerConfiguration.forResource(ConfigMap.class); // when operator.register(fooReconciler, configuration); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java index d0bb8916cb..497fafb897 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -107,6 +108,63 @@ private io.javaoperatorsdk.operator.api.config.ControllerConfiguration create return new AnnotationControllerConfiguration<>(reconciler); } + @ControllerConfiguration(namespaces = "foo") + private static class WatchCurrentReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return null; + } + } + + @Test + void overridingNamespacesShouldWork() { + var configuration = createConfiguration(new WatchCurrentReconciler()); + assertEquals(Set.of("foo"), configuration.getNamespaces()); + assertFalse(configuration.watchAllNamespaces()); + assertFalse(configuration.watchCurrentNamespace()); + + configuration = ControllerConfigurationOverrider.override(configuration) + .addingNamespaces("foo", "bar") + .build(); + assertEquals(Set.of("foo", "bar"), configuration.getNamespaces()); + assertFalse(configuration.watchAllNamespaces()); + assertFalse(configuration.watchCurrentNamespace()); + + configuration = ControllerConfigurationOverrider.override(configuration) + .removingNamespaces("bar") + .build(); + assertEquals(Set.of("foo"), configuration.getNamespaces()); + assertFalse(configuration.watchAllNamespaces()); + assertFalse(configuration.watchCurrentNamespace()); + + configuration = ControllerConfigurationOverrider.override(configuration) + .removingNamespaces("foo") + .build(); + assertTrue(configuration.watchAllNamespaces()); + assertFalse(configuration.watchCurrentNamespace()); + + configuration = ControllerConfigurationOverrider.override(configuration) + .settingNamespace("foo") + .build(); + assertFalse(configuration.watchAllNamespaces()); + assertFalse(configuration.watchCurrentNamespace()); + assertEquals(Set.of("foo"), configuration.getNamespaces()); + + configuration = ControllerConfigurationOverrider.override(configuration) + .watchingOnlyCurrentNamespace() + .build(); + assertFalse(configuration.watchAllNamespaces()); + assertTrue(configuration.watchCurrentNamespace()); + + configuration = ControllerConfigurationOverrider.override(configuration) + .watchingAllNamespaces() + .build(); + assertTrue(configuration.watchAllNamespaces()); + assertFalse(configuration.watchCurrentNamespace()); + } + @Test void configuredDependentShouldNotChangeOnParentOverrideEvenWhenInitialConfigIsSame() { var configuration = createConfiguration(new OverriddenNSOnDepReconciler()); @@ -140,7 +198,7 @@ void dependentShouldWatchAllNamespacesIfParentDoesAsWell() { var config = extractFirstDependentKubernetesResourceConfig(configuration); // check that the DependentResource inherits the controller's configuration if applicable - assertEquals(0, config.namespaces().size()); + assertTrue(ResourceConfiguration.allNamespacesWatched(config.namespaces())); // override the NS final var newNS = "bar"; @@ -160,7 +218,7 @@ void shouldBePossibleToForceDependentToWatchAllNamespaces() { var config = extractFirstDependentKubernetesResourceConfig(configuration); // check that the DependentResource inherits the controller's configuration if applicable - assertEquals(0, config.namespaces().size()); + assertTrue(ResourceConfiguration.allNamespacesWatched(config.namespaces())); // override the NS final var newNS = "bar"; @@ -169,7 +227,7 @@ void shouldBePossibleToForceDependentToWatchAllNamespaces() { // check that dependent config is still configured to watch all NS config = extractFirstDependentKubernetesResourceConfig(configuration); - assertEquals(0, config.namespaces().size()); + assertTrue(ResourceConfiguration.allNamespacesWatched(config.namespaces())); } @Test @@ -224,7 +282,8 @@ void replaceNamedDependentResourceConfigShouldWork() { final var dependentResourceName = DependentResource.defaultNameFor(ReadOnlyDependent.class); assertTrue(dependents.stream().anyMatch(dr -> dr.getName().equals(dependentResourceName))); - var dependentSpec = dependents.stream().filter(dr -> dr.getName().equals(dependentResourceName)) + var dependentSpec = dependents.stream() + .filter(dr -> dr.getName().equals(dependentResourceName)) .findFirst().get(); assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); var maybeConfig = dependentSpec.getDependentResourceConfiguration(); @@ -292,7 +351,7 @@ public ReadOnlyDependent() { } } - @KubernetesDependent(namespaces = KubernetesDependent.WATCH_ALL_NAMESPACES) + @KubernetesDependent(namespaces = Constants.WATCH_ALL_NAMESPACES) private static class WatchAllNSDependent extends KubernetesDependentResource { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java new file mode 100644 index 0000000000..a58e3b5b73 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockControllerConfiguration { + @SuppressWarnings({"unchecked", "rawtypes"}) + public static ControllerConfiguration forResource( + Class resourceType) { + final ControllerConfiguration configuration = mock(ControllerConfiguration.class); + when(configuration.getResourceClass()).thenReturn(resourceType); + when(configuration.getNamespaces()).thenReturn(ResourceConfiguration.DEFAULT_NAMESPACES); + when(configuration.getEffectiveNamespaces()).thenCallRealMethod(); + return configuration; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ResourceConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ResourceConfigurationTest.java new file mode 100644 index 0000000000..013eec9cc0 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ResourceConfigurationTest.java @@ -0,0 +1,71 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.javaoperatorsdk.operator.api.reconciler.Constants; + +import static org.junit.jupiter.api.Assertions.*; + +class ResourceConfigurationTest { + + @Test + void allNamespacesWatched() { + assertThrows(IllegalArgumentException.class, + () -> ResourceConfiguration.allNamespacesWatched(null)); + assertThrows(IllegalArgumentException.class, () -> ResourceConfiguration.allNamespacesWatched( + Set.of(Constants.WATCH_CURRENT_NAMESPACE, Constants.WATCH_ALL_NAMESPACES, "foo"))); + assertThrows(IllegalArgumentException.class, () -> ResourceConfiguration.allNamespacesWatched( + Collections.emptySet())); + assertFalse(ResourceConfiguration.allNamespacesWatched(Set.of("foo", "bar"))); + assertTrue(ResourceConfiguration.allNamespacesWatched(Set.of(Constants.WATCH_ALL_NAMESPACES))); + assertFalse(ResourceConfiguration.allNamespacesWatched(Set.of("foo"))); + assertFalse( + ResourceConfiguration.allNamespacesWatched(Set.of(Constants.WATCH_CURRENT_NAMESPACE))); + } + + @Test + void currentNamespaceWatched() { + assertThrows(IllegalArgumentException.class, + () -> ResourceConfiguration.currentNamespaceWatched(null)); + assertThrows(IllegalArgumentException.class, + () -> ResourceConfiguration.currentNamespaceWatched( + Set.of(Constants.WATCH_CURRENT_NAMESPACE, Constants.WATCH_ALL_NAMESPACES, "foo"))); + assertThrows(IllegalArgumentException.class, + () -> ResourceConfiguration.currentNamespaceWatched(Collections.emptySet())); + assertFalse(ResourceConfiguration.currentNamespaceWatched(Set.of("foo", "bar"))); + assertFalse( + ResourceConfiguration.currentNamespaceWatched(Set.of(Constants.WATCH_ALL_NAMESPACES))); + assertFalse(ResourceConfiguration.currentNamespaceWatched(Set.of("foo"))); + assertTrue( + ResourceConfiguration.currentNamespaceWatched(Set.of(Constants.WATCH_CURRENT_NAMESPACE))); + } + + @Test + void nullLabelSelectorByDefault() { + assertNull(new ResourceConfiguration<>() {}.getLabelSelector()); + } + + @Test + void shouldWatchAllNamespacesByDefault() { + assertTrue(new ResourceConfiguration<>() {}.watchAllNamespaces()); + } + + @Test + void failIfNotValid() { + assertThrows(IllegalArgumentException.class, () -> ResourceConfiguration.failIfNotValid(null)); + assertThrows(IllegalArgumentException.class, + () -> ResourceConfiguration.failIfNotValid(Collections.emptySet())); + assertThrows(IllegalArgumentException.class, () -> ResourceConfiguration.failIfNotValid( + Set.of(Constants.WATCH_CURRENT_NAMESPACE, Constants.WATCH_ALL_NAMESPACES, "foo"))); + assertThrows(IllegalArgumentException.class, () -> ResourceConfiguration.failIfNotValid( + Set.of(Constants.WATCH_CURRENT_NAMESPACE, "foo"))); + assertThrows(IllegalArgumentException.class, () -> ResourceConfiguration.failIfNotValid( + Set.of(Constants.WATCH_ALL_NAMESPACES, "foo"))); + + // should work + ResourceConfiguration.failIfNotValid(Set.of("foo", "bar")); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index 26fd763467..67e9112b27 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -13,17 +13,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; -@SuppressWarnings("unchecked") +@SuppressWarnings({"unchecked", "rawtypes"}) class ControllerTest { - final ControllerConfiguration configuration = mock(ControllerConfiguration.class); final Reconciler reconciler = mock(Reconciler.class); @Test void crdShouldNotBeCheckedForNativeResources() { final var client = MockKubernetesClient.client(Secret.class); - - when(configuration.getResourceClass()).thenReturn(Secret.class); + final var configuration = MockControllerConfiguration.forResource(Secret.class); final var controller = new Controller(reconciler, configuration, client); controller.start(); @@ -33,7 +31,7 @@ void crdShouldNotBeCheckedForNativeResources() { @Test void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { final var client = MockKubernetesClient.client(TestCustomResource.class); - when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); + final var configuration = MockControllerConfiguration.forResource(TestCustomResource.class); try { ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(false)); @@ -49,10 +47,10 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { @Test void usesFinalizerIfThereIfReconcilerImplementsCleaner() { Reconciler reconciler = mock(Reconciler.class, withSettings().extraInterfaces(Cleaner.class)); - when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); + final var configuration = MockControllerConfiguration.forResource(Secret.class); - final var controller = new Controller(reconciler, - configuration, MockKubernetesClient.client(TestCustomResource.class)); + final var controller = new Controller(reconciler, configuration, + MockKubernetesClient.client(Secret.class)); assertThat(controller.useFinalizer()).isTrue(); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index 42a9a75447..f09605d095 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -7,7 +7,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; @@ -140,8 +140,7 @@ void retrievingAnEventSourceWhenMultipleAreRegisteredForATypeShouldRequireAQuali } private EventSourceManager initManager() { - final ControllerConfiguration configuration = mock(ControllerConfiguration.class); - when(configuration.getResourceClass()).thenReturn(HasMetadata.class); + final var configuration = MockControllerConfiguration.forResource(HasMetadata.class); final Controller controller = new Controller(mock(Reconciler.class), configuration, MockKubernetesClient.client(HasMetadata.class)); return new EventSourceManager(controller); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index d1b6883075..6afec04543 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -21,6 +21,7 @@ import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.Controller; @@ -64,15 +65,13 @@ static void classSetup() { * equals will fail on the two equal but NOT identical TestCustomResources because equals is not * implemented on TestCustomResourceSpec or TestCustomResourceStatus */ - ConfigurationServiceProvider.overrideCurrent(overrider -> { - overrider.checkingCRDAndValidateLocalModel(false) - .withResourceCloner(new Cloner() { - @Override - public R clone(R object) { - return object; - } - }); - }); + ConfigurationServiceProvider.overrideCurrent(overrider -> overrider + .checkingCRDAndValidateLocalModel(false).withResourceCloner(new Cloner() { + @Override + public R clone(R object) { + return object; + } + })); } @AfterAll @@ -92,17 +91,19 @@ private ReconciliationDispatcher init(R customResourc Reconciler reconciler, ControllerConfiguration configuration, CustomResourceFacade customResourceFacade, boolean useFinalizer) { - configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; + final Class resourceClass = (Class) customResource.getClass(); + configuration = configuration == null ? MockControllerConfiguration.forResource(resourceClass) + : configuration; when(configuration.getFinalizerName()).thenReturn(DEFAULT_FINALIZER); when(configuration.getName()).thenReturn("EventDispatcherTestController"); - when(configuration.getResourceClass()).thenReturn((Class) customResource.getClass()); + when(configuration.getResourceClass()).thenReturn(resourceClass); when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); when(configuration.reconciliationMaxInterval()) .thenReturn(Optional.of(Duration.ofHours(RECONCILIATION_MAX_INTERVAL))); Controller controller = new Controller<>(reconciler, configuration, - MockKubernetesClient.client(customResource.getClass())) { + MockKubernetesClient.client(resourceClass)) { @Override public boolean useFinalizer() { return useFinalizer; @@ -114,7 +115,7 @@ public boolean useFinalizer() { } @Test - void addFinalizerOnNewResource() throws Exception { + void addFinalizerOnNewResource() { assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, never()) @@ -126,7 +127,7 @@ void addFinalizerOnNewResource() throws Exception { } @Test - void callCreateOrUpdateOnNewResourceIfFinalizerSet() throws Exception { + void callCreateOrUpdateOnNewResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, times(1)) @@ -134,7 +135,7 @@ void callCreateOrUpdateOnNewResourceIfFinalizerSet() throws Exception { } @Test - void updatesOnlyStatusSubResourceIfFinalizerSet() throws Exception { + void updatesOnlyStatusSubResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.updateStatus(testCustomResource); @@ -146,7 +147,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() throws Exception { } @Test - void updatesBothResourceAndStatusIfFinalizerSet() throws Exception { + void updatesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.updateResourceAndStatus(testCustomResource); @@ -187,7 +188,7 @@ void patchesStatus() { } @Test - void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() throws Exception { + void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -261,7 +262,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { } @Test - void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() throws Exception { + void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -272,7 +273,7 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() throws Exce } @Test - void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() throws Exception { + void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -295,8 +296,7 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { } @Test - void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() - throws Exception { + void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -305,7 +305,7 @@ void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet } @Test - void propagatesRetryInfoToContextIfFinalizerSet() throws Exception { + void propagatesRetryInfoToContextIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution( @@ -334,7 +334,7 @@ public boolean isLastAttempt() { } @Test - void setReScheduleToPostExecutionControlFromUpdateControl() throws Exception { + void setReScheduleToPostExecutionControlFromUpdateControl() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = @@ -368,7 +368,7 @@ void setObservedGenerationForStatusIfNeeded() throws Exception { Reconciler reconciler = mock(Reconciler.class); ControllerConfiguration config = - mock(ControllerConfiguration.class); + MockControllerConfiguration.forResource(ObservedGenCustomResource.class); CustomResourceFacade facade = mock(CustomResourceFacade.class); var dispatcher = init(observedGenResource, reconciler, config, facade, true); @@ -389,8 +389,7 @@ void updatesObservedGenerationOnNoUpdateUpdateControl() throws Exception { var observedGenResource = createObservedGenCustomResource(); Reconciler reconciler = mock(Reconciler.class); - ControllerConfiguration config = - mock(ControllerConfiguration.class); + final var config = MockControllerConfiguration.forResource(ObservedGenCustomResource.class); CustomResourceFacade facade = mock(CustomResourceFacade.class); when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())) @@ -410,8 +409,7 @@ void updateObservedGenerationOnCustomResourceUpdate() throws Exception { var observedGenResource = createObservedGenCustomResource(); Reconciler reconciler = mock(Reconciler.class); - ControllerConfiguration config = - mock(ControllerConfiguration.class); + final var config = MockControllerConfiguration.forResource(ObservedGenCustomResource.class); CustomResourceFacade facade = mock(CustomResourceFacade.class); when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())) @@ -428,7 +426,7 @@ void updateObservedGenerationOnCustomResourceUpdate() throws Exception { } @Test - void callErrorStatusHandlerIfImplemented() throws Exception { + void callErrorStatusHandlerIfImplemented() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> { @@ -460,7 +458,7 @@ public boolean isLastAttempt() { } @Test - void callErrorStatusHandlerEvenOnFirstError() throws Exception { + void callErrorStatusHandlerEvenOnFirstError() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> { @@ -523,7 +521,7 @@ void errorHandlerCanInstructNoRetryNoUpdate() { } @Test - void schedulesReconciliationIfMaxDelayIsSet() throws Exception { + void schedulesReconciliationIfMaxDelayIsSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -536,7 +534,7 @@ void schedulesReconciliationIfMaxDelayIsSet() throws Exception { } @Test - void canSkipSchedulingMaxDelayIf() throws Exception { + void canSkipSchedulingMaxDelayIf() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -573,6 +571,7 @@ public ExecutionScope executionScopeWithCREvent(T res private class TestReconciler implements Reconciler, Cleaner, ErrorStatusHandler { + private BiFunction> reconcile; private BiFunction cleanup; private ErrorStatusHandler errorHandler; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 7ac6779523..8563fe4d01 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -14,6 +14,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.cache.Indexer; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -23,6 +24,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +@SuppressWarnings({"rawtypes", "unchecked"}) class InformerEventSourceTest { private static final String PREV_RESOURCE_VERSION = "0"; @@ -30,16 +32,17 @@ class InformerEventSourceTest { private static final String NEXT_RESOURCE_VERSION = "2"; private InformerEventSource informerEventSource; - private KubernetesClient clientMock = mock(KubernetesClient.class); - private TemporaryResourceCache temporaryResourceCacheMock = + private final KubernetesClient clientMock = mock(KubernetesClient.class); + private final TemporaryResourceCache temporaryResourceCacheMock = mock(TemporaryResourceCache.class); - private EventHandler eventHandlerMock = mock(EventHandler.class); - private MixedOperation crClientMock = mock(MixedOperation.class); - private FilterWatchListMultiDeletable specificResourceClientMock = + private final EventHandler eventHandlerMock = mock(EventHandler.class); + private final MixedOperation crClientMock = mock(MixedOperation.class); + private final FilterWatchListMultiDeletable specificResourceClientMock = mock(FilterWatchListMultiDeletable.class); - private FilterWatchListDeletable labeledResourceClientMock = mock(FilterWatchListDeletable.class); - private SharedIndexInformer informer = mock(SharedIndexInformer.class); - private InformerConfiguration informerConfiguration = + private final FilterWatchListDeletable labeledResourceClientMock = + mock(FilterWatchListDeletable.class); + private final SharedIndexInformer informer = mock(SharedIndexInformer.class); + private final InformerConfiguration informerConfiguration = mock(InformerConfiguration.class); @BeforeEach @@ -51,6 +54,8 @@ void setup() { when(labeledResourceClientMock.runnableInformer(0)).thenReturn(informer); when(informer.getIndexer()).thenReturn(mock(Indexer.class)); + when(informerConfiguration.getEffectiveNamespaces()) + .thenReturn(ResourceConfiguration.DEFAULT_NAMESPACES); when(informerConfiguration.getSecondaryToPrimaryMapper()) .thenReturn(mock(SecondaryToPrimaryMapper.class)); From 3c3c5bd60b84a58c827622ceb8b2ccc93540e645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 22 Apr 2022 17:37:53 +0200 Subject: [PATCH 0010/1176] All Resource Event Sources can handle multiple seconday resources for a primary resources (#1169) --- .../api/reconciler/DefaultContext.java | 16 +- .../dependent/RecentOperationCacheFiller.java | 2 +- .../processing/MultiResourceOwner.java | 23 --- .../AbstractCachingDependentResource.java | 4 +- .../AbstractPollingDependentResource.java | 9 +- .../PerResourcePollingDependentResource.java | 7 +- .../external/PollingDependentResource.java | 22 ++- .../ExternalResourceCachingEventSource.java | 60 ------- .../event/source/AbstractEventSource.java | 1 - .../source/AbstractResourceEventSource.java | 2 +- .../event/source/CacheKeyMapper.java | 18 ++ .../event/source/CachingEventSource.java | 7 +- .../ExternalResourceCachingEventSource.java | 166 ++++++++++++++++++ .../event/source/ResourceEventSource.java | 16 ++ .../ControllerResourceEventSource.java | 6 + .../inbound/CachingInboundEventSource.java | 26 ++- .../source/informer/InformerEventSource.java | 13 +- .../informer/ManagedInformerEventSource.java | 4 +- .../PerResourcePollingEventSource.java | 114 ++++++------ .../source/polling/PollingEventSource.java | 65 +++---- .../EventSourceInitializerTest.java | 5 +- ...xternalResourceCachingEventSourceTest.java | 108 +++++++++--- .../event/source/SampleExternalResource.java | 4 +- .../PerResourcePollingEventSourceTest.java | 76 +++++--- .../polling/PollingEventSourceTest.java | 36 ++-- .../dependent/SchemaDependentResource.java | 18 +- .../SchemaPollingResourceFetcher.java | 24 --- 27 files changed, 517 insertions(+), 335 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MultiResourceOwner.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java delete mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceFetcher.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 00a94390c6..0b6d90065b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.api.reconciler; -import java.util.Collections; -import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -10,7 +8,6 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.processing.MultiResourceOwner; public class DefaultContext

implements Context

{ @@ -37,17 +34,8 @@ public Optional getRetryInfo() { @SuppressWarnings("unchecked") public Set getSecondaryResources(Class expectedType) { return controller.getEventSourceManager().getEventSourcesFor(expectedType).stream() - .map( - es -> { - if (es instanceof MultiResourceOwner) { - return ((MultiResourceOwner) es).getSecondaryResources(primaryResource); - } else { - return es.getSecondaryResource(primaryResource) - .map(List::of) - .orElse(Collections.emptyList()); - } - }) - .flatMap(List::stream) + .map(es -> es.getSecondaryResources(primaryResource)) + .flatMap(Set::stream) .collect(Collectors.toSet()); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java index 7e27537d49..cb84783a4f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java @@ -6,5 +6,5 @@ public interface RecentOperationCacheFiller { void handleRecentResourceCreate(ResourceID resourceID, R resource); - void handleRecentResourceUpdate(ResourceID resourceID, R resource, R previousResourceVersion); + void handleRecentResourceUpdate(ResourceID resourceID, R resource, R previousVersionOfResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MultiResourceOwner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MultiResourceOwner.java deleted file mode 100644 index d3cc6b3770..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MultiResourceOwner.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.javaoperatorsdk.operator.processing; - -import java.util.List; -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; - -public interface MultiResourceOwner extends ResourceOwner { - - default Optional getSecondaryResource(P primary) { - var list = getSecondaryResources(primary); - if (list.isEmpty()) { - return Optional.empty(); - } else if (list.size() == 1) { - return Optional.of(list.get(0)); - } else { - throw new IllegalStateException("More than 1 secondary resource related to primary"); - } - - } - - List getSecondaryResources(P primary); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java index 036a507de9..4f85cb9d1d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.dependent.AbstractEventSourceHolderDependentResource; -import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; public abstract class AbstractCachingDependentResource extends @@ -15,8 +15,6 @@ protected AbstractCachingDependentResource(Class resourceType) { this.resourceType = resourceType; } - public abstract Optional fetchResource(P primaryResource); - @Override public Class resourceType() { return resourceType; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java index 8c91dea15d..f3812a095f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java @@ -1,9 +1,10 @@ package io.javaoperatorsdk.operator.processing.dependent.external; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.source.CacheKeyMapper; public abstract class AbstractPollingDependentResource - extends AbstractCachingDependentResource { + extends AbstractCachingDependentResource implements CacheKeyMapper { public static final int DEFAULT_POLLING_PERIOD = 5000; private long pollingPeriod; @@ -24,4 +25,10 @@ public void setPollingPeriod(long pollingPeriod) { public long getPollingPeriod() { return pollingPeriod; } + + // for now dependent resources support event sources only with one owned resource. + @Override + public String keyFor(R resource) { + return CacheKeyMapper.singleResourceCacheKeyMapper().keyFor(resource); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java index b598073a21..1afa566a3a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java @@ -2,12 +2,14 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; public abstract class PerResourcePollingDependentResource extends AbstractPollingDependentResource implements PerResourcePollingEventSource.ResourceFetcher { + + public PerResourcePollingDependentResource(Class resourceType) { super(resourceType); } @@ -20,6 +22,7 @@ public PerResourcePollingDependentResource(Class resourceType, long pollingPe protected ExternalResourceCachingEventSource createEventSource( EventSourceContext

context) { return new PerResourcePollingEventSource<>(this, context.getPrimaryCache(), - getPollingPeriod(), resourceType()); + getPollingPeriod(), resourceType(), this); } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java index 1cc6e22afe..9fb293b1f5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java @@ -1,28 +1,32 @@ package io.javaoperatorsdk.operator.processing.dependent.external; -import java.util.Map; -import java.util.function.Supplier; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.CacheKeyMapper; +import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PollingEventSource; public abstract class PollingDependentResource - extends AbstractPollingDependentResource implements Supplier> { + extends AbstractPollingDependentResource + implements PollingEventSource.GenericResourceFetcher { + + private final CacheKeyMapper cacheKeyMapper; - public PollingDependentResource(Class resourceType) { + public PollingDependentResource(Class resourceType, CacheKeyMapper cacheKeyMapper) { super(resourceType); + this.cacheKeyMapper = cacheKeyMapper; } - public PollingDependentResource(Class resourceType, long pollingPeriod) { + public PollingDependentResource(Class resourceType, long pollingPeriod, + CacheKeyMapper cacheKeyMapper) { super(resourceType, pollingPeriod); + this.cacheKeyMapper = cacheKeyMapper; } @Override protected ExternalResourceCachingEventSource createEventSource( EventSourceContext

context) { - return new PollingEventSource<>(this, getPollingPeriod(), resourceType()); + return new PollingEventSource<>(this, getPollingPeriod(), resourceType(), cacheKeyMapper); } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java deleted file mode 100644 index b4bb0f7ef7..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; -import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; - -public class ExternalResourceCachingEventSource - extends CachingEventSource implements RecentOperationCacheFiller { - - public ExternalResourceCachingEventSource(Class resourceClass) { - super(resourceClass); - } - - public synchronized void handleDelete(ResourceID relatedResourceID) { - if (!isRunning()) { - return; - } - var cachedValue = cache.get(relatedResourceID); - cache.remove(relatedResourceID); - // we only propagate event if the resource was previously in cache - if (cachedValue.isPresent()) { - getEventHandler().handleEvent(new Event(relatedResourceID)); - } - } - - public synchronized void handleEvent(R value, ResourceID relatedResourceID) { - if (!isRunning()) { - return; - } - var cachedValue = cache.get(relatedResourceID); - if (cachedValue.map(v -> !v.equals(value)).orElse(true)) { - cache.put(relatedResourceID, value); - getEventHandler().handleEvent(new Event(relatedResourceID)); - } - } - - @Override - public synchronized void handleRecentResourceCreate(ResourceID resourceID, R resource) { - if (cache.get(resourceID).isEmpty()) { - cache.put(resourceID, resource); - } - } - - @Override - public synchronized void handleRecentResourceUpdate(ResourceID resourceID, R resource, - R previousResourceVersion) { - cache.get(resourceID).ifPresent(r -> { - if (r.equals(previousResourceVersion)) { - cache.put(resourceID, resource); - } - }); - } - - @Override - public Optional getSecondaryResource(P primary) { - return cache.get(ResourceID.fromResource(primary)); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java index 6767e974e2..a6666ba81d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java @@ -4,7 +4,6 @@ import io.javaoperatorsdk.operator.processing.event.EventHandler; public abstract class AbstractEventSource implements EventSource { - private EventHandler handler; private volatile boolean running = false; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java index b22899f246..051a75ff20 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java @@ -2,7 +2,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; -public abstract class AbstractResourceEventSource

+public abstract class AbstractResourceEventSource extends AbstractEventSource implements ResourceEventSource { private final Class resourceClass; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java new file mode 100644 index 0000000000..d290e15496 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CacheKeyMapper.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +public interface CacheKeyMapper { + + String keyFor(R resource); + + /** + * Used if a polling event source handles only single secondary resource. See also docs for: + * {@link ExternalResourceCachingEventSource} + * + * @return static id mapper, all resources are mapped for same id. + * @param secondary resource type + */ + static CacheKeyMapper singleResourceCacheKeyMapper() { + return r -> "id"; + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index 8453651b6b..55bd1ab920 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -14,7 +14,7 @@ * @param represents the type of resources (usually external non-kubernetes ones) being handled. */ public abstract class CachingEventSource - extends AbstractResourceEventSource implements Cache { + extends AbstractResourceEventSource implements Cache { protected UpdatableCache cache; @@ -43,12 +43,9 @@ public Stream list(Predicate predicate) { return cache.list(predicate); } - protected UpdatableCache initCache() { - return new ConcurrentHashMapCache<>(); - } - public Optional getCachedValue(ResourceID resourceID) { return cache.get(resourceID); } + protected abstract UpdatableCache initCache(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java new file mode 100644 index 0000000000..f8a0cafcd8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java @@ -0,0 +1,166 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +/** + * Handles caching and related operation of external event sources. It can handle multiple secondary + * resources for a single primary resources. + *

+ * There are two related concepts to understand: + *

    + *
  • CacheKeyMapper - maps/extracts a key used to reference the associated resource in the + * cache
  • + *
  • Object equals usage - compares if the two resources are the same or same version.
  • + *
+ * + * When a resource is added for a primary resource its key is used to put in a map. Equals is used + * to compare if it's still the same resource, or an updated version of it. Event is emitted only if + * a new resource(s) is received or actually updated or deleted. Delete is detected by a missing + * key. + * + * @param type of polled external secondary resource + * @param

primary resource + */ +public abstract class ExternalResourceCachingEventSource + extends AbstractResourceEventSource implements RecentOperationCacheFiller { + + private static Logger log = LoggerFactory.getLogger(ExternalResourceCachingEventSource.class); + + protected final CacheKeyMapper cacheKeyMapper; + + protected Map> cache = new ConcurrentHashMap<>(); + + protected ExternalResourceCachingEventSource(Class resourceClass, + CacheKeyMapper cacheKeyMapper) { + super(resourceClass); + this.cacheKeyMapper = cacheKeyMapper; + } + + protected synchronized void handleDelete(ResourceID primaryID) { + var res = cache.remove(primaryID); + if (res != null) { + getEventHandler().handleEvent(new Event(primaryID)); + } + } + + protected synchronized void handleDeletes(ResourceID primaryID, Set resource) { + handleDelete(primaryID, + resource.stream().map(cacheKeyMapper::keyFor).collect(Collectors.toSet())); + } + + protected synchronized void handleDelete(ResourceID primaryID, R resource) { + handleDelete(primaryID, Set.of(cacheKeyMapper.keyFor(resource))); + } + + protected synchronized void handleDelete(ResourceID primaryID, Set resourceID) { + if (!isRunning()) { + return; + } + var cachedValues = cache.get(primaryID); + var sizeBeforeRemove = cachedValues.size(); + resourceID.forEach(cachedValues::remove); + + if (cachedValues.isEmpty()) { + cache.remove(primaryID); + } + if (sizeBeforeRemove > cachedValues.size()) { + getEventHandler().handleEvent(new Event(primaryID)); + } + } + + protected synchronized void handleResources(ResourceID primaryID, R actualResource) { + handleResources(primaryID, Set.of(actualResource), true); + } + + protected synchronized void handleResources(ResourceID primaryID, Set newResources) { + handleResources(primaryID, newResources, true); + } + + protected synchronized void handleResources(Map> allNewResources) { + var toDelete = cache.keySet().stream().filter(k -> !allNewResources.containsKey(k)) + .collect(Collectors.toList()); + toDelete.forEach(this::handleDelete); + allNewResources.forEach((primaryID, resources) -> handleResources(primaryID, resources)); + } + + protected synchronized void handleResources(ResourceID primaryID, Set newResources, + boolean propagateEvent) { + log.debug("Handling resources update for: {} numberOfResources: {} ", primaryID, + newResources.size()); + if (!isRunning()) { + return; + } + var cachedResources = cache.get(primaryID); + var newResourcesMap = + newResources.stream().collect(Collectors.toMap(cacheKeyMapper::keyFor, r -> r)); + cache.put(primaryID, newResourcesMap); + if (propagateEvent && !newResourcesMap.equals(cachedResources)) { + getEventHandler().handleEvent(new Event(primaryID)); + } + } + + @Override + public synchronized void handleRecentResourceCreate(ResourceID primaryID, R resource) { + var actualValues = cache.get(primaryID); + var resourceId = cacheKeyMapper.keyFor(resource); + if (actualValues == null) { + actualValues = new HashMap<>(); + cache.put(primaryID, actualValues); + actualValues.put(resourceId, resource); + } else { + actualValues.computeIfAbsent(resourceId, r -> resource); + } + } + + @Override + public synchronized void handleRecentResourceUpdate( + ResourceID primaryID, R resource, R previousVersionOfResource) { + var actualValues = cache.get(primaryID); + if (actualValues != null) { + var resourceId = cacheKeyMapper.keyFor(resource); + R actualResource = actualValues.get(resourceId); + if (actualResource.equals(previousVersionOfResource)) { + actualValues.put(resourceId, resource); + } + } + } + + @Override + public Set getSecondaryResources(P primary) { + return getSecondaryResources(ResourceID.fromResource(primary)); + } + + public Set getSecondaryResources(ResourceID primaryID) { + var cachedValues = cache.get(primaryID); + if (cachedValues == null) { + return Collections.emptySet(); + } else { + return new HashSet<>(cache.get(primaryID).values()); + } + } + + public Optional getSecondaryResource(ResourceID primaryID) { + var resources = getSecondaryResources(primaryID); + if (resources.isEmpty()) { + return Optional.empty(); + } else if (resources.size() == 1) { + return Optional.of(resources.iterator().next()); + } else { + throw new IllegalStateException("More than 1 secondary resource related to primary"); + } + } + + public Map> getCache() { + return Collections.unmodifiableMap(cache); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java index d65208f746..d57a662d82 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java @@ -1,9 +1,25 @@ package io.javaoperatorsdk.operator.processing.event.source; +import java.util.Optional; +import java.util.Set; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.ResourceOwner; public interface ResourceEventSource extends EventSource, ResourceOwner { + default Optional getSecondaryResource(P primary) { + var resources = getSecondaryResources(primary); + if (resources.isEmpty()) { + return Optional.empty(); + } else if (resources.size() == 1) { + return Optional.of(resources.iterator().next()); + } else { + throw new IllegalStateException("More than 1 secondary resource related to primary"); + } + + } + + Set getSecondaryResources(P primary); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index c3ffc326bf..0c0f0c6b8e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; import java.util.Optional; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,4 +95,9 @@ public void onDelete(T resource, boolean b) { public Optional getSecondaryResource(T primary) { throw new IllegalStateException("This method should not be called here. Primary: " + primary); } + + @Override + public Set getSecondaryResources(T primary) { + throw new IllegalStateException("This method should not be called here. Primary: " + primary); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java index 34f37a048b..6d421a6460 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java @@ -1,21 +1,29 @@ package io.javaoperatorsdk.operator.processing.event.source.inbound; +import java.util.Set; + import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.CacheKeyMapper; +import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; + +public class CachingInboundEventSource + extends ExternalResourceCachingEventSource { -public class CachingInboundEventSource - extends ExternalResourceCachingEventSource { + public CachingInboundEventSource(Class resourceClass, CacheKeyMapper cacheKeyMapper) { + super(resourceClass, cacheKeyMapper); + } - public CachingInboundEventSource(Class resourceClass) { - super(resourceClass); + public void handleResourceEvent(ResourceID primaryID, Set resources) { + super.handleResources(primaryID, resources); } - public void handleResourceEvent(T resource, ResourceID relatedResourceID) { - super.handleEvent(resource, relatedResourceID); + public void handleResourceEvent(ResourceID primaryID, R resource) { + super.handleResources(primaryID, resource); } - public void handleResourceDeleteEvent(ResourceID resourceID) { - super.handleDelete(resourceID); + public void handleResourceDeleteEvent(ResourceID primaryID, String resourceID) { + super.handleDelete(primaryID, Set.of(resourceID)); } + } 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 08facecbb7..5d1d12875f 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 @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; -import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -13,7 +13,6 @@ import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationEventFilter; -import io.javaoperatorsdk.operator.processing.MultiResourceOwner; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -66,7 +65,7 @@ */ public class InformerEventSource extends ManagedInformerEventSource> - implements MultiResourceOwner, ResourceEventHandler, RecentOperationEventFilter { + implements ResourceEventHandler, RecentOperationEventFilter { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); @@ -177,11 +176,11 @@ private void propagateEvent(R object) { } @Override - public List getSecondaryResources(P primary) { + public Set getSecondaryResources(P primary) { var secondaryIDs = primaryToSecondaryIndex.getSecondaryResources(ResourceID.fromResource(primary)); return secondaryIDs.stream().map(this::get).flatMap(Optional::stream) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); } public InformerConfiguration getConfiguration() { @@ -190,9 +189,9 @@ public InformerConfiguration getConfiguration() { @Override public synchronized void handleRecentResourceUpdate(ResourceID resourceID, R resource, - R previousResourceVersion) { + R previousVersionOfResource) { handleRecentCreateOrUpdate(resource, - () -> super.handleRecentResourceUpdate(resourceID, resource, previousResourceVersion)); + () -> super.handleRecentResourceUpdate(resourceID, resource, previousVersionOfResource)); } @Override 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 606b0bc962..95aba48ad7 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 @@ -74,9 +74,9 @@ public void stop() { @Override public void handleRecentResourceUpdate(ResourceID resourceID, R resource, - R previousResourceVersion) { + R previousVersionOfResource) { temporaryResourceCache.putUpdatedResource(resource, - previousResourceVersion.getMetadata().getResourceVersion()); + previousVersionOfResource.getMetadata().getResourceVersion()); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java index 8b04dc9e57..f55e7dd05e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java @@ -1,9 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.polling; -import java.util.Map; -import java.util.Optional; -import java.util.Timer; -import java.util.TimerTask; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; @@ -12,10 +9,11 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.Cache; +import io.javaoperatorsdk.operator.processing.event.source.CacheKeyMapper; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; /** @@ -41,40 +39,36 @@ public class PerResourcePollingEventSource private final Cache

resourceCache; private final Predicate

registerPredicate; private final long period; + private final Set fetchedForPrimaries = ConcurrentHashMap.newKeySet(); public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, Cache

resourceCache, long period, Class resourceClass) { - this(resourceFetcher, resourceCache, period, null, resourceClass); + this(resourceFetcher, resourceCache, period, null, resourceClass, + CacheKeyMapper.singleResourceCacheKeyMapper()); + } + + public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, + Cache

resourceCache, long period, Class resourceClass, + CacheKeyMapper cacheKeyMapper) { + this(resourceFetcher, resourceCache, period, null, resourceClass, cacheKeyMapper); } public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, Cache

resourceCache, long period, - Predicate

registerPredicate, Class resourceClass) { - super(resourceClass); + Predicate

registerPredicate, Class resourceClass, + CacheKeyMapper cacheKeyMapper) { + super(resourceClass, cacheKeyMapper); this.resourceFetcher = resourceFetcher; this.resourceCache = resourceCache; this.period = period; this.registerPredicate = registerPredicate; } - private void pollForResource(P resource) { - var value = resourceFetcher.fetchResource(resource); - var resourceID = ResourceID.fromResource(resource); - if (value.isEmpty()) { - super.handleDelete(resourceID); - } else { - super.handleEvent(value.get(), resourceID); - } - } - - private Optional getAndCacheResource(ResourceID resourceID) { - var resource = resourceCache.get(resourceID); - if (resource.isPresent()) { - var value = resourceFetcher.fetchResource(resource.get()); - value.ifPresent(v -> cache.put(resourceID, v)); - return value; - } - return Optional.empty(); + private Set getAndCacheResource(P primary, boolean fromGetter) { + var values = resourceFetcher.fetchResources(primary); + handleResources(ResourceID.fromResource(primary), values, !fromGetter); + fetchedForPrimaries.add(ResourceID.fromResource(primary)); + return values; } @Override @@ -95,7 +89,8 @@ public void onResourceDeleted(P resource) { log.debug("Canceling task for resource: {}", resource); task.cancel(); } - cache.remove(resourceID); + handleDelete(resourceID); + fetchedForPrimaries.remove(resourceID); } // This method is always called from the same Thread for the same resource, @@ -103,24 +98,27 @@ public void onResourceDeleted(P resource) { // important // because otherwise there will be a race condition related to the timerTasks. private void checkAndRegisterTask(P resource) { - var resourceID = ResourceID.fromResource(resource); - if (timerTasks.get(resourceID) == null && (registerPredicate == null + var primaryID = ResourceID.fromResource(resource); + if (timerTasks.get(primaryID) == null && (registerPredicate == null || registerPredicate.test(resource))) { - var task = new TimerTask() { - @Override - public void run() { - if (!isRunning()) { - log.debug("Event source not yet started. Will not run for: {}", resourceID); - return; - } - // always use up-to-date resource from cache - var res = resourceCache.get(resourceID); - res.ifPresentOrElse(r -> pollForResource(r), - () -> log.warn("No resource in cache for resource ID: {}", resourceID)); - } - }; - timerTasks.put(resourceID, task); - timer.schedule(task, 0, period); + var task = + new TimerTask() { + @Override + public void run() { + if (!isRunning()) { + log.debug("Event source not yet started. Will not run for: {}", primaryID); + return; + } + // always use up-to-date resource from cache + var res = resourceCache.get(primaryID); + res.ifPresentOrElse(p -> getAndCacheResource(p, false), + () -> log.warn("No resource in cache for resource ID: {}", primaryID)); + } + }; + timerTasks.put(primaryID, task); + // there is a delay, to not do two fetches when the resources first appeared + // and getSecondaryResource is called on reconciliation. + timer.schedule(task, period, period); } } @@ -132,28 +130,22 @@ public void run() { * @return the related resource for this event source */ @Override - public Optional getSecondaryResource(P primary) { - return getValueFromCacheOrSupplier(ResourceID.fromResource(primary)); - } - - /** - * - * @param resourceID of the target related resource - * @return the cached value of the resource, if not present it gets the resource from the - * supplier. The value provided from the supplier is cached, but no new event is - * propagated. - */ - public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { - var cachedValue = getCachedValue(resourceID); - if (cachedValue.isPresent()) { - return cachedValue; + public Set getSecondaryResources(P primary) { + var primaryID = ResourceID.fromResource(primary); + var cachedValue = cache.get(primaryID); + if (cachedValue != null && !cachedValue.isEmpty()) { + return new HashSet<>(cachedValue.values()); } else { - return getAndCacheResource(resourceID); + if (fetchedForPrimaries.contains(primaryID)) { + return Collections.emptySet(); + } else { + return getAndCacheResource(primary, true); + } } } public interface ResourceFetcher { - Optional fetchResource(P primaryResource); + Set fetchResources(P primaryResource); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java index dcdb2cdda9..09ff2e8b0e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java @@ -1,33 +1,33 @@ package io.javaoperatorsdk.operator.processing.event.source.polling; import java.util.Map; -import java.util.Optional; +import java.util.Set; import java.util.Timer; import java.util.TimerTask; -import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.CacheKeyMapper; +import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; /** * Polls resource (on contrary to {@link PerResourcePollingEventSource}) not per resource bases but - * instead to calls supplier periodically and independently of the number of state of custom - * resources managed by the operator. It is called on start (synced). This means that when the - * reconciler first time executed on startup a poll already happened before. So if the cache does - * not contain the target resource it means it is not created yet or was deleted while an operator - * was not running. + * instead to calls supplier periodically and independently of the number or state of custom + * resources managed by the controller. It is called on start (synced). This means that when the + * reconciler first time executed on startup the first poll already happened before. So if the cache + * does not contain the target resource it means it is not created yet or was deleted while an + * operator was not running. * *

* Another caveat with this is if the cached object is checked in the reconciler and created since * not in the cache it should be manually added to the cache, since it can happen that the * reconciler is triggered before the cache is propagated with the new resource from a scheduled - * execution. See {@link #put(ResourceID, Object)} method. So the generic workflow in reconciler - * should be: + * execution. See {@link #handleRecentResourceCreate(ResourceID, Object)} and update method. So the + * generic workflow in reconciler should be: * *