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 dbd09a32cc..ab13a6d5d5 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 @@ -247,7 +247,9 @@ public List getDependentResources() { Set.of(dependent.dependsOn()), instantiateIfNotDefault(dependent.readyPostcondition(), Condition.class, context), instantiateIfNotDefault(dependent.reconcilePrecondition(), Condition.class, context), - instantiateIfNotDefault(dependent.deletePostcondition(), Condition.class, context)); + instantiateIfNotDefault(dependent.deletePostcondition(), Condition.class, context), + Constants.NO_VALUE_SET.equals(dependent.eventSource()) ? null + : dependent.eventSource()); specsMap.put(name, spec); } @@ -286,13 +288,13 @@ private Object createKubernetesResourceConfig(Class OnDeleteFilter onDeleteFilter = null; GenericFilter genericFilter = null; ResourceDiscriminator resourceDiscriminator = null; + String eventSourceNameToUse = null; if (kubeDependent != null) { if (!Arrays.equals(KubernetesDependent.DEFAULT_NAMESPACES, kubeDependent.namespaces())) { namespaces = Set.of(kubeDependent.namespaces()); configuredNS = true; } - final var fromAnnotation = kubeDependent.labelSelector(); labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation; 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 e9cae2dccf..454bd4ca49 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 @@ -174,7 +174,7 @@ private void replaceConfig(String name, Object newConfig, DependentResourceSpec< namedDependentResourceSpecs.put(name, new DependentResourceSpec<>(current.getDependentResourceClass(), newConfig, name, current.getDependsOn(), current.getReadyCondition(), current.getReconcileCondition(), - current.getDeletePostCondition())); + current.getDeletePostCondition(), current.getEventSourceName().orElse(null))); } @SuppressWarnings("unchecked") @@ -220,7 +220,8 @@ public ControllerConfiguration build() { KubernetesDependentResourceConfig c) { return new DependentResourceSpec(spec.getDependentResourceClass(), c.setNamespaces(namespaces), name, spec.getDependsOn(), spec.getReadyCondition(), - spec.getReconcileCondition(), spec.getDeletePostCondition()); + spec.getReconcileCondition(), spec.getDeletePostCondition(), + (String) spec.getEventSourceName().orElse(null)); } public static ControllerConfigurationOverrider override( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java index f146d127d0..fadd0a9816 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java @@ -23,9 +23,12 @@ public class DependentResourceSpec, C> { private final Condition deletePostCondition; + private final String eventSourceName; + public DependentResourceSpec(Class dependentResourceClass, C dependentResourceConfig, String name, Set dependsOn, Condition readyCondition, - Condition reconcileCondition, Condition deletePostCondition) { + Condition reconcileCondition, Condition deletePostCondition, + String eventSourceName) { this.dependentResourceClass = dependentResourceClass; this.dependentResourceConfig = dependentResourceConfig; this.name = name; @@ -33,6 +36,7 @@ public DependentResourceSpec(Class dependentResourceClass, C dependentResourc this.readyCondition = readyCondition; this.reconcileCondition = reconcileCondition; this.deletePostCondition = deletePostCondition; + this.eventSourceName = eventSourceName; } public Class getDependentResourceClass() { @@ -89,4 +93,8 @@ public Condition getReconcileCondition() { public Condition getDeletePostCondition() { return deletePostCondition; } + + public Optional getEventSourceName() { + return Optional.ofNullable(eventSourceName); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index e8062e9651..2c1e82ba4c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -3,6 +3,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; +import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; @@ -13,14 +15,14 @@ */ public class EventSourceContext

{ - private final IndexerResourceCache

primaryCache; + private final EventSourceManager

eventSourceManager; private final ControllerConfiguration

controllerConfiguration; private final KubernetesClient client; - public EventSourceContext(IndexerResourceCache

primaryCache, + public EventSourceContext(EventSourceManager

eventSourceManager, ControllerConfiguration

controllerConfiguration, KubernetesClient client) { - this.primaryCache = primaryCache; + this.eventSourceManager = eventSourceManager; this.controllerConfiguration = controllerConfiguration; this.client = client; } @@ -31,7 +33,7 @@ public EventSourceContext(IndexerResourceCache

primaryCache, * @return the primary resource cache */ public IndexerResourceCache

getPrimaryCache() { - return primaryCache; + return eventSourceManager.getControllerResourceEventSource(); } /** @@ -54,4 +56,8 @@ public ControllerConfiguration

getControllerConfiguration() { public KubernetesClient getClient() { return client; } + + public EventSourceRetriever

getEventSourceRetriever() { + return eventSourceManager; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 9b3c7a67bd..c49332b468 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -1,9 +1,13 @@ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceAware; import io.javaoperatorsdk.operator.processing.event.source.EventSource; /** @@ -39,6 +43,21 @@ static Map nameEventSources(EventSource... eventSources) { return eventSourceMap; } + @SuppressWarnings("unchecked,rawtypes") + static Map nameEventSourcesFromDependentResource( + EventSourceContext context, DependentResource... dependentResources) { + if (dependentResources != null) { + Map eventSourceMap = new HashMap<>(dependentResources.length); + Arrays.stream(dependentResources) + .filter(EventSourceAware.class::isInstance) + .forEach(esa -> ((EventSourceAware) esa).eventSource(context) + .ifPresent(es -> eventSourceMap.put(generateNameFor(es), es))); + return eventSourceMap; + } else { + return Collections.emptyMap(); + } + } + /** * This is for the use case when the event sources are not access explicitly by name in the * reconciler. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java index 90ba701a6a..2f2f1f1e0d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java @@ -57,4 +57,13 @@ * one can be */ String[] dependsOn() default {}; + + /** + * Setting here a name of the event source means that dependent resource will use an event source + * registered with that name. So won't create one. This is helpful if more dependent resources + * created for the same type, and want to share a common event source. + * + * @return event source name (if any) provided by the dependent resource should be used. + */ + String eventSource() default NO_VALUE_SET; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceAware.java new file mode 100644 index 0000000000..c4f4fbcc4f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceAware.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; + +public interface EventSourceAware { + + /** + * Dependent resources are designed to by default provide event sources. There are cases where it + * might not: + *

    + *
  • If an event source is shared between multiple dependent resources. In this case only one or + * none of the dependent resources sharing the event source should provide one.
  • + *
  • Some special implementation of an event source. That just execute some action might not + * provide one.
  • + *
+ * + * @param context context of event source initialization + * @return an optional event source + */ + default Optional> eventSource(EventSourceContext

context) { + return getUsedEventSourceName().map( + name -> context.getEventSourceRetriever().getResourceEventSourceFor(resourceType(), name)); + } + + Class resourceType(); + + void useEventSourceNamed(String eventSourceName); + + default Optional getUsedEventSourceName() { + return Optional.empty(); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java index 98190cb7ef..c83af1270a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java @@ -4,6 +4,11 @@ import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +/** + * @deprecated now event source related methods are directly on {@link DependentResource} + * @param

primary resource + */ +@Deprecated(forRemoval = true) public interface EventSourceProvider

{ /** * @param context - event source context where the event source is initialized diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java index c83da1c8ea..468e14e8ea 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java @@ -1,14 +1,14 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; public class ReconcileResult { - private final R resource; - private final Operation operation; + private final Map resourceOperations; public static ReconcileResult resourceCreated(T resource) { return new ReconcileResult<>(resource, Operation.CREATED); @@ -22,25 +22,49 @@ public static ReconcileResult noOperation(T resource) { return new ReconcileResult<>(resource, Operation.NONE); } + @SafeVarargs + public static ReconcileResult aggregatedResult(ReconcileResult... results) { + if (results == null) { + throw new IllegalArgumentException("Should provide results to aggregate"); + } + if (results.length == 1) { + return results[0]; + } + final Map operations = new HashMap<>(results.length); + for (ReconcileResult res : results) { + res.getSingleResource().ifPresent(r -> operations.put(r, res.getSingleOperation())); + } + return new ReconcileResult<>(operations); + } + @Override public String toString() { - return getResource() - .map(r -> r instanceof HasMetadata ? ResourceID.fromResource((HasMetadata) r) : r) - .orElse("no resource") - + " -> " + operation; + return resourceOperations.entrySet().stream().collect(Collectors.toMap( + e -> e instanceof HasMetadata ? ResourceID.fromResource((HasMetadata) e) : e, + Map.Entry::getValue)) + .toString(); } private ReconcileResult(R resource, Operation operation) { - this.resource = resource; - this.operation = operation; + resourceOperations = resource != null ? Map.of(resource, operation) : Collections.emptyMap(); + } + + private ReconcileResult(Map operations) { + resourceOperations = Collections.unmodifiableMap(operations); + } + + public Optional getSingleResource() { + return resourceOperations.entrySet().stream().findFirst().map(Map.Entry::getKey); } - public Optional getResource() { - return Optional.ofNullable(resource); + public Operation getSingleOperation() { + return resourceOperations.entrySet().stream().findFirst().map(Map.Entry::getValue) + .orElseThrow(); } - public Operation getOperation() { - return operation; + @SuppressWarnings("unused") + public Map getResourceOperations() { + return resourceOperations; } public enum Operation { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 92b70e722d..f5ee0cd269 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -32,6 +32,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; @@ -207,21 +208,26 @@ private void initContextIfNeeded(P resource, Context

context) { } public void initAndRegisterEventSources(EventSourceContext

context) { - managedWorkflow - .getDependentResourcesByName().entrySet().stream() - .filter(drEntry -> drEntry.getValue() instanceof EventSourceProvider) - .forEach(drEntry -> { - final var provider = (EventSourceProvider) drEntry.getValue(); - final var source = provider.initEventSource(context); - eventSourceManager.registerEventSource(drEntry.getKey(), source); - }); - - // add manually defined event sources if (reconciler instanceof EventSourceInitializer) { final var provider = (EventSourceInitializer

) this.reconciler; final var ownSources = provider.prepareEventSources(context); ownSources.forEach(eventSourceManager::registerEventSource); } + managedWorkflow.getDependentResourcesByName().entrySet().stream().filter(entry -> { + final var value = entry.getValue(); + return value instanceof EventSourceProvider || value instanceof EventSourceAware; + }).forEach(entry -> { + final var value = entry.getValue(); + final var key = entry.getKey(); + if (value instanceof EventSourceProvider) { + final var provider = (EventSourceProvider) value; + final var source = provider.initEventSource(context); + eventSourceManager.registerEventSource(key, source); + } else { + ((EventSourceAware) value).eventSource(context) + .ifPresent(es -> eventSourceManager.registerEventSource(key, es)); + } + }); } @Override @@ -288,8 +294,8 @@ public synchronized void start(boolean startEventProcessor) throws OperatorExcep try { // check that the custom resource is known by the cluster if configured that way validateCRDWithLocalModelIfRequired(resClass, controllerName, crdName, specVersion); - final var context = new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource(), configuration, kubernetesClient); + final var context = + new EventSourceContext<>(eventSourceManager, configuration, kubernetesClient); initAndRegisterEventSources(context); eventSourceManager.start(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java index 1abfb3df4b..e3d399b67a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.processing.dependent; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.slf4j.Logger; @@ -20,25 +22,73 @@ public abstract class AbstractDependentResource protected final boolean creatable = this instanceof Creator; protected final boolean updatable = this instanceof Updater; + protected final boolean bulk = this instanceof BulkDependentResource; protected Creator creator; protected Updater updater; + protected BulkDependentResource bulkDependentResource; - private ResourceDiscriminator resourceDiscriminator; + protected List> resourceDiscriminator = new ArrayList<>(1); @SuppressWarnings("unchecked") public AbstractDependentResource() { creator = creatable ? (Creator) this : null; updater = updatable ? (Updater) this : null; + + bulkDependentResource = bulk ? (BulkDependentResource) this : null; } @Override public ReconcileResult reconcile(P primary, Context

context) { - Optional maybeActual = getSecondaryResource(primary, context); + if (bulk) { + final var count = bulkDependentResource.count(primary, context); + deleteBulkResourcesIfRequired(count, lastKnownBulkSize(), primary, context); + adjustDiscriminators(count); + @SuppressWarnings("unchecked") + final ReconcileResult[] results = new ReconcileResult[count]; + for (int i = 0; i < count; i++) { + results[i] = reconcileIndexAware(primary, i, context); + } + return ReconcileResult.aggregatedResult(results); + } else { + return reconcileIndexAware(primary, 0, context); + } + } + + protected void deleteBulkResourcesIfRequired(int targetCount, int actualCount, P primary, + Context

context) { + if (targetCount >= actualCount) { + return; + } + for (int i = targetCount; i < actualCount; i++) { + var resource = getSecondaryResourceIndexAware(primary, i, context); + var index = i; + resource.ifPresent( + r -> bulkDependentResource.deleteBulkResourceWithIndex(primary, r, index, context)); + } + } + + private void adjustDiscriminators(int count) { + if (resourceDiscriminator.size() == count) { + return; + } + if (resourceDiscriminator.size() < count) { + for (int i = resourceDiscriminator.size(); i < count; i++) { + resourceDiscriminator.add(bulkDependentResource.getResourceDiscriminator(i)); + } + } + if (resourceDiscriminator.size() > count) { + resourceDiscriminator.subList(count, resourceDiscriminator.size()).clear(); + } + } + + protected ReconcileResult reconcileIndexAware(P primary, int i, Context

context) { + Optional maybeActual = bulk ? getSecondaryResourceIndexAware(primary, i, context) + : getSecondaryResource(primary, context); if (creatable || updatable) { if (maybeActual.isEmpty()) { if (creatable) { - var desired = desired(primary, context); + var desired = desiredIndexAware(primary, i, context); throwIfNull(desired, primary, "Desired"); logForOperation("Creating", primary, desired); var createdResource = handleCreate(desired, primary, context); @@ -47,9 +97,15 @@ public ReconcileResult reconcile(P primary, Context

context) { } else { final var actual = maybeActual.get(); if (updatable) { - final var match = updater.match(actual, primary, context); + final Matcher.Result match; + if (bulk) { + match = updater.match(actual, primary, i, context); + } else { + match = updater.match(actual, primary, context); + } if (!match.matched()) { - final var desired = match.computedDesired().orElse(desired(primary, context)); + final var desired = + match.computedDesired().orElse(desiredIndexAware(primary, i, context)); throwIfNull(desired, primary, "Desired"); logForOperation("Updating", primary, desired); var updatedResource = handleUpdate(actual, desired, primary, context); @@ -67,9 +123,18 @@ public ReconcileResult reconcile(P primary, Context

context) { return ReconcileResult.noOperation(maybeActual.orElse(null)); } + private R desiredIndexAware(P primary, int i, Context

context) { + return bulk ? desired(primary, i, context) + : desired(primary, context); + } + public Optional getSecondaryResource(P primary, Context

context) { - return resourceDiscriminator == null ? context.getSecondaryResource(resourceType()) - : resourceDiscriminator.distinguish(resourceType(), primary, context); + return resourceDiscriminator.isEmpty() ? context.getSecondaryResource(resourceType()) + : resourceDiscriminator.get(0).distinguish(resourceType(), primary, context); + } + + protected Optional getSecondaryResourceIndexAware(P primary, int index, Context

context) { + return context.getSecondaryResource(resourceType(), resourceDiscriminator.get(index)); } private void throwIfNull(R desired, P primary, String descriptor) { @@ -97,7 +162,7 @@ protected R handleCreate(R desired, P primary, Context

context) { } /** - * Allows sub-classes to perform additional processing (e.g. caching) on the created resource if + * Allows subclasses to perform additional processing (e.g. caching) on the created resource if * needed. * * @param primaryResourceId the {@link ResourceID} of the primary resource associated with the @@ -107,7 +172,7 @@ protected R handleCreate(R desired, P primary, Context

context) { protected abstract void onCreated(ResourceID primaryResourceId, R created); /** - * Allows sub-classes to perform additional processing on the updated resource if needed. + * Allows subclasses to perform additional processing on the updated resource if needed. * * @param primaryResourceId the {@link ResourceID} of the primary resource associated with the * newly updated resource @@ -129,12 +194,28 @@ protected R desired(P primary, Context

context) { "desired method must be implemented if this DependentResource can be created and/or updated"); } - public void setResourceDiscriminator( + protected R desired(P primary, int index, Context

context) { + throw new IllegalStateException( + "Must be implemented for bulk DependentResource creation"); + } + + public AbstractDependentResource setResourceDiscriminator( ResourceDiscriminator resourceDiscriminator) { - this.resourceDiscriminator = resourceDiscriminator; + if (resourceDiscriminator != null) { + this.resourceDiscriminator.add(resourceDiscriminator); + } + return this; } public ResourceDiscriminator getResourceDiscriminator() { - return resourceDiscriminator; + if (this.resourceDiscriminator.isEmpty()) { + return null; + } else { + return this.resourceDiscriminator.get(0); + } + } + + protected int lastKnownBulkSize() { + return resourceDiscriminator.size(); } } 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 be0db98393..06532200ae 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 @@ -1,12 +1,13 @@ package io.javaoperatorsdk.operator.processing.dependent; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; @@ -15,8 +16,7 @@ @Ignore public abstract class AbstractEventSourceHolderDependentResource> - extends AbstractDependentResource - implements EventSourceProvider

{ + extends AbstractDependentResource implements EventSourceAware { private T eventSource; private final Class resourceType; @@ -25,12 +25,16 @@ public abstract class AbstractEventSourceHolderDependentResource onUpdateFilter; protected OnDeleteFilter onDeleteFilter; protected GenericFilter genericFilter; + protected String eventSourceToUse; protected AbstractEventSourceHolderDependentResource(Class resourceType) { this.resourceType = resourceType; } - public EventSource initEventSource(EventSourceContext

context) { + + @SuppressWarnings("unchecked") + @Override + public Optional> eventSource(EventSourceContext

context) { // some sub-classes (e.g. KubernetesDependentResource) can have their event source created // before this method is called in the managed case, so only create the event source if it // hasn't already been set. @@ -38,12 +42,18 @@ public EventSource initEventSource(EventSourceContext

context) { // event source // is shared between dependent resources this does not override the existing filters. if (eventSource == null) { - eventSource = createEventSource(context); + T localEventSource = + (T) EventSourceAware.super.eventSource(context).orElse(createEventSource(context)); + setEventSource(localEventSource); applyFilters(); } + return Optional.of(eventSource); + } - isCacheFillerEventSource = eventSource instanceof RecentOperationCacheFiller; - return eventSource; + /** To make this backwards compatible even for respect of overriding */ + @SuppressWarnings("unchecked") + public T initEventSource(EventSourceContext

context) { + return (T) eventSource(context).orElseThrow(); } @Override @@ -54,6 +64,7 @@ public Class resourceType() { protected abstract T createEventSource(EventSourceContext

context); protected void setEventSource(T eventSource) { + isCacheFillerEventSource = eventSource instanceof RecentOperationCacheFiller; this.eventSource = eventSource; } @@ -64,8 +75,8 @@ protected void applyFilters() { this.eventSource.setGenericFilter(genericFilter); } - protected T eventSource() { - return eventSource; + public Optional> eventSource() { + return Optional.ofNullable(eventSource); } protected void onCreated(ResourceID primaryResourceId, R created) { @@ -96,4 +107,14 @@ public void setOnUpdateFilter(OnUpdateFilter onUpdateFilter) { public void setOnDeleteFilter(OnDeleteFilter onDeleteFilter) { this.onDeleteFilter = onDeleteFilter; } + + @Override + public void useEventSourceNamed(String eventSourceName) { + this.eventSourceToUse = eventSourceName; + } + + @Override + public Optional getUsedEventSourceName() { + return Optional.ofNullable(eventSourceToUse); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResource.java new file mode 100644 index 0000000000..1f2688f5cb --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResource.java @@ -0,0 +1,35 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; + +/** + * Manages dynamic number of resources created for a primary resource. Since the point of a bulk + * dependent resource is to manage the number of secondary resources dynamically it implement + * {@link Creator} and {@link Deleter} interfaces out of the box. A concrete dependent resource can + * implement additionally also {@link Updater}. + */ +public interface BulkDependentResource extends Creator, Deleter

{ + + /** + * @return number of resources to create + */ + int count(P primary, Context

context); + + R desired(P primary, int index, Context

context); + + /** + * Used to delete resource if the desired count is lower than the actual count of a resource. + * + * @param primary resource + * @param resource actual resource from the cache for the index + * @param i index of the resource + * @param context actual context + */ + void deleteBulkResourceWithIndex(P primary, R resource, int i, Context

context); + + ResourceDiscriminator getResourceDiscriminator(int index); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkResourceDiscriminatorFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkResourceDiscriminatorFactory.java new file mode 100644 index 0000000000..8b9b6e968d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkResourceDiscriminatorFactory.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; + +public interface BulkResourceDiscriminatorFactory { + + ResourceDiscriminator createResourceDiscriminator(int index); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkUpdater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkUpdater.java new file mode 100644 index 0000000000..9c00b47d0c --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkUpdater.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +/** + * Helper for the Bulk Dependent Resources to make it more explicit that bulk needs to only + * implement the index aware match method. + * + * @param secondary resource type + * @param

primary resource type + */ +public interface BulkUpdater extends Updater { + + default Matcher.Result match(R actualResource, P primary, Context

context) { + throw new IllegalStateException(); + } + + Matcher.Result match(R actualResource, P primary, int index, Context

context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java index 459d7951d6..1d3b34a47b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java @@ -16,4 +16,10 @@ public Result match(R actualResource, P primary, Context

context) { var desired = abstractDependentResource.desired(primary, context); return Result.computed(actualResource.equals(desired), desired); } + + @Override + public Result match(R actualResource, P primary, int index, Context

context) { + var desired = abstractDependentResource.desired(primary, index, context); + return Result.computed(actualResource.equals(desired), desired); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java index 750fe89cbf..835f76ab3a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java @@ -95,4 +95,19 @@ public Optional computedDesired() { * {@link Result#computed(boolean, Object)}) */ Result match(R actualResource, P primary, Context

context); + + /** + * Determines whether the specified secondary resource matches the desired state with target index + * of a bulk resource as defined from the specified primary resource, given the specified + * {@link Context}. + * + * @param actualResource the resource we want to determine whether it's matching the desired state + * @param primary the primary resource from which the desired state is inferred + * @param context the context in which the resource is being matched + * @return a {@link Result} encapsulating whether the resource matched its desired state and this + * associated state if it was computed as part of the matching process. Use the static + * convenience methods ({@link Result#nonComputed(boolean)} and + * {@link Result#computed(boolean, Object)}) + */ + Result match(R actualResource, P primary, int index, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java index 828f9ad785..06b3cb52f6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java @@ -8,4 +8,8 @@ public interface Updater { R update(R actual, R desired, P primary, Context

context); Result match(R actualResource, P primary, Context

context); + + default Result match(R actualResource, P primary, int index, Context

context) { + throw new IllegalStateException("Implement this for bulk matching"); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java deleted file mode 100644 index 748452c30c..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.external; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; -import io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.DesiredEqualsMatcher; -import io.javaoperatorsdk.operator.processing.dependent.Matcher; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.ConcurrentHashMapCache; -import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; - -/** A base class for external dependent resources that don't have an event source. */ -@Ignore -public abstract class AbstractSimpleDependentResource - extends AbstractDependentResource { - - // cache serves only to keep the resource readable again until next reconciliation when the - // new resource is read again. - protected final UpdatableCache cache; - protected Matcher matcher; - - public AbstractSimpleDependentResource() { - this(new ConcurrentHashMapCache<>()); - } - - public AbstractSimpleDependentResource(UpdatableCache cache) { - this.cache = cache; - initMatcher(); - } - - @Override - public Optional getSecondaryResource(P primaryResource, Context

context) { - return cache.get(ResourceID.fromResource(primaryResource)); - } - - /** - * Actually read the resource from the target API - * - * @param primaryResource the primary associated resource - * @return fetched resource if present - **/ - public abstract Optional fetchResource(HasMetadata primaryResource); - - @Override - public ReconcileResult reconcile(P primary, Context

context) { - var resourceId = ResourceID.fromResource(primary); - Optional resource = fetchResource(primary); - resource.ifPresentOrElse(r -> cache.put(resourceId, r), () -> cache.remove(resourceId)); - return super.reconcile(primary, context); - } - - public final void delete(P primary, Context

context) { - deleteResource(primary, context); - cache.remove(ResourceID.fromResource(primary)); - } - - protected abstract void deleteResource(P primary, Context

context); - - @Override - protected void onCreated(ResourceID primaryResourceId, R created) { - cache.put(primaryResourceId, created); - } - - @Override - protected void onUpdated(ResourceID primaryResourceId, R updated, R actual) { - cache.put(primaryResourceId, updated); - } - - public Matcher.Result match(R actualResource, P primary, Context

context) { - return matcher.match(actualResource, primary, context); - } - - protected void initMatcher() { - matcher = new DesiredEqualsMatcher<>(this); - } - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index e294b1c938..bb066b5b24 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -24,17 +24,42 @@ private GenericKubernetesResourceMatcher(KubernetesDependentResource depen static Matcher matcherFor( Class resourceType, KubernetesDependentResource dependentResource) { if (Secret.class.isAssignableFrom(resourceType)) { - return (actual, primary, context) -> { - final var desired = dependentResource.desired(primary, context); - return Result.computed( - ResourceComparators.compareSecretData((Secret) desired, (Secret) actual), desired); + return new Matcher<>() { + @Override + public Result match(R actualResource, P primary, Context

context) { + final var desired = dependentResource.desired(primary, context); + return Result.computed( + ResourceComparators.compareSecretData((Secret) desired, (Secret) actualResource), + desired); + } + + @Override + public Result match(R actualResource, P primary, int index, Context

context) { + final var desired = dependentResource.desired(primary, index, context); + return Result.computed( + ResourceComparators.compareSecretData((Secret) desired, (Secret) actualResource), + desired); + } }; } else if (ConfigMap.class.isAssignableFrom(resourceType)) { - return (actual, primary, context) -> { - final var desired = dependentResource.desired(primary, context); - return Result.computed( - ResourceComparators.compareConfigMapData((ConfigMap) desired, (ConfigMap) actual), - desired); + return new Matcher<>() { + @Override + public Result match(R actualResource, P primary, Context

context) { + final var desired = dependentResource.desired(primary, context); + return Result.computed( + ResourceComparators.compareConfigMapData((ConfigMap) desired, + (ConfigMap) actualResource), + desired); + } + + @Override + public Result match(R actualResource, P primary, int index, Context

context) { + final var desired = dependentResource.desired(primary, index, context); + return Result.computed( + ResourceComparators.compareConfigMapData((ConfigMap) desired, + (ConfigMap) actualResource), + desired); + } }; } else { return new GenericKubernetesResourceMatcher(dependentResource); @@ -43,32 +68,18 @@ static Matcher matcherFor( @Override public Result match(R actualResource, P primary, Context

context) { - return match(dependentResource, actualResource, primary, context, false); + var desired = dependentResource.desired(primary, context); + return match(desired, actualResource, false); } - /** - * Determines whether the specified actual resource matches the desired state defined by the - * specified {@link KubernetesDependentResource} based on the observed state of the associated - * specified primary resource. - * - * @param dependentResource the {@link KubernetesDependentResource} implementation used to - * computed the desired state associated with the specified primary resource - * @param actualResource the observed dependent resource for which we want to determine whether it - * matches the desired state or not - * @param primary the primary resource from which we want to compute the desired state - * @param context the {@link Context} instance within which this method is called - * @param considerMetadata {@code true} to consider the metadata of the actual resource when - * determining if it matches the desired state, {@code false} if matching should occur only - * considering the spec of the resources - * @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object - * @param the type of resource we want to determine whether they match or not - * @param

the type of primary resources associated with the secondary resources we want to - * match - */ - public static Result match( - KubernetesDependentResource dependentResource, R actualResource, P primary, - Context

context, boolean considerMetadata) { - final var desired = dependentResource.desired(primary, context); + @Override + public Result match(R actualResource, P primary, int index, Context

context) { + var desired = dependentResource.desired(primary, index, context); + return match(desired, actualResource, false); + } + + public static Result match( + R desired, R actualResource, boolean considerMetadata) { if (considerMetadata) { final var desiredMetadata = desired.getMetadata(); final var actualMetadata = actualResource.getMetadata(); @@ -95,4 +106,30 @@ public static Result match( } return Result.computed(true, desired); } + + /** + * Determines whether the specified actual resource matches the desired state defined by the + * specified {@link KubernetesDependentResource} based on the observed state of the associated + * specified primary resource. + * + * @param dependentResource the {@link KubernetesDependentResource} implementation used to + * computed the desired state associated with the specified primary resource + * @param actualResource the observed dependent resource for which we want to determine whether it + * matches the desired state or not + * @param primary the primary resource from which we want to compute the desired state + * @param context the {@link Context} instance within which this method is called + * @param considerMetadata {@code true} to consider the metadata of the actual resource when + * determining if it matches the desired state, {@code false} if matching should occur only + * considering the spec of the resources + * @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object + * @param the type of resource we want to determine whether they match or not + * @param

the type of primary resources associated with the secondary resources we want to + * match + */ + public static Result match( + KubernetesDependentResource dependentResource, R actualResource, P primary, + Context

context, boolean considerMetadata) { + final var desired = dependentResource.desired(primary, context); + return match(desired, actualResource, considerMetadata); + } } 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 603f4ae62e..2ccd4da82a 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 @@ -72,4 +72,6 @@ Class genericFilter() default GenericFilter.class; Class resourceDiscriminator() default ResourceDiscriminator.class; + + String eventSourceToUse() default NO_VALUE_SET; } 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 328a061e6b..df195fad29 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 @@ -31,7 +31,7 @@ public abstract class KubernetesDependentResource extends AbstractEventSourceHolderDependentResource> implements KubernetesClientAware, - DependentResourceConfigurator { + DependentResourceConfigurator> { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); @@ -52,9 +52,14 @@ public KubernetesDependentResource(Class resourceType) { : GenericResourceUpdatePreProcessor.processorFor(resourceType); } + @SuppressWarnings("unchecked") @Override - public void configureWith(KubernetesDependentResourceConfig config) { + public void configureWith(KubernetesDependentResourceConfig config) { this.kubernetesDependentResourceConfig = config; + var discriminator = kubernetesDependentResourceConfig.getResourceDiscriminator(); + if (discriminator != null) { + setResourceDiscriminator(discriminator); + } } private void configureWith(String labelSelector, Set namespaces, @@ -134,12 +139,25 @@ public Result match(R actualResource, P primary, Context

context) { return matcher.match(actualResource, primary, context); } + public Result match(R actualResource, P primary, int index, Context

context) { + return matcher.match(actualResource, primary, index, context); + } + public void delete(P primary, Context

context) { - getSecondaryResource(primary, context).ifPresent(r -> client.resource(r).delete()); + if (bulk) { + deleteBulkResourcesIfRequired(0, lastKnownBulkSize(), primary, context); + } else { + var resource = getSecondaryResource(primary, context); + resource.ifPresent(r -> client.resource(r).delete()); + } } - @SuppressWarnings("unchecked") - protected Resource prepare(R desired, P primary, String actionName) { + public void deleteBulkResourceWithIndex(P primary, R resource, int i, Context

context) { + client.resource(resource).delete(); + } + + protected Resource prepare(R desired, + P primary, String actionName) { log.debug("{} target resource with type: {}, with id: {}", actionName, desired.getClass(), @@ -149,9 +167,7 @@ protected Resource prepare(R desired, P primary, String actionName) { } else if (useDefaultAnnotationsToIdentifyPrimary()) { addDefaultSecondaryToPrimaryMapperAnnotations(desired, primary); } - Class targetClass = (Class) desired.getClass(); - return client.resources(targetClass).inNamespace(desired.getMetadata().getNamespace()) - .resource(desired); + return client.resource(desired).inNamespace(desired.getMetadata().getNamespace()); } @Override @@ -159,12 +175,15 @@ protected Resource prepare(R desired, P primary, String actionName) { protected InformerEventSource createEventSource(EventSourceContext

context) { if (kubernetesDependentResourceConfig != null) { // sets the filters for the dependent resource, which are applied by parent class + onAddFilter = kubernetesDependentResourceConfig.onAddFilter(); onUpdateFilter = kubernetesDependentResourceConfig.onUpdateFilter(); onDeleteFilter = kubernetesDependentResourceConfig.onDeleteFilter(); genericFilter = kubernetesDependentResourceConfig.genericFilter(); - setResourceDiscriminator(kubernetesDependentResourceConfig.getResourceDiscriminator()); - + var discriminator = kubernetesDependentResourceConfig.getResourceDiscriminator(); + if (discriminator != null) { + setResourceDiscriminator(discriminator); + } configureWith(kubernetesDependentResourceConfig.labelSelector(), kubernetesDependentResourceConfig.namespaces(), !kubernetesDependentResourceConfig.wereNamespacesConfigured(), context); @@ -175,7 +194,7 @@ protected InformerEventSource createEventSource(EventSourceContext

cont "Using default configuration for {} KubernetesDependentResource, call configureWith to provide configuration", resourceType().getSimpleName()); } - return eventSource(); + return (InformerEventSource) eventSource().orElseThrow(); } private boolean useDefaultAnnotationsToIdentifyPrimary() { @@ -215,11 +234,18 @@ protected R desired(P primary, Context

context) { return super.desired(primary, context); } + @Override + protected R desired(P primary, int index, Context

context) { + return super.desired(primary, index, context); + } + private void prepareEventFiltering(R desired, ResourceID resourceID) { - eventSource().prepareForCreateOrUpdateEventFiltering(resourceID, desired); + ((InformerEventSource) eventSource().orElseThrow()) + .prepareForCreateOrUpdateEventFiltering(resourceID, desired); } private void cleanupAfterEventFiltering(ResourceID resourceID) { - eventSource().cleanupOnCreateOrUpdateEventFiltering(resourceID); + ((InformerEventSource) eventSource().orElseThrow()) + .cleanupOnCreateOrUpdateEventFiltering(resourceID); } } 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 e2a2c0f684..188b805fc4 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 @@ -17,7 +17,6 @@ public class KubernetesDependentResourceConfig { private String labelSelector = NO_VALUE_SET; private boolean namespacesWereConfigured = false; private ResourceDiscriminator resourceDiscriminator; - private OnAddFilter onAddFilter; private OnUpdateFilter onUpdateFilter; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java index aad9475518..b34d58af74 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java @@ -15,6 +15,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.workflow.builder.WorkflowBuilder; @@ -78,10 +79,14 @@ public DependentResource createAndConfigureFrom(DependentResourceSpec spec, final var dependentResource = ConfigurationServiceProvider.instance().dependentResourceFactory().createFrom(spec); + if (dependentResource instanceof EventSourceAware) { + EventSourceAware eventSourceAware = (EventSourceAware) dependentResource; + spec.getEventSourceName().ifPresent(n -> eventSourceAware.useEventSourceNamed((String) n)); + } + if (dependentResource instanceof KubernetesClientAware) { ((KubernetesClientAware) dependentResource).setKubernetesClient(client); } - if (dependentResource instanceof DependentResourceConfigurator) { final var configurator = (DependentResourceConfigurator) dependentResource; spec.getDependentResourceConfiguration().ifPresent(configurator::configureWith); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java index a31d8902b0..421092fb6a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java @@ -11,7 +11,7 @@ ResourceEventSource getResourceEventSourceFor( Class dependentType); ResourceEventSource getResourceEventSourceFor( - Class dependentType, String qualifier); + Class dependentType, String name); List> getResourceEventSourcesFor(Class dependentType); 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 6e486b5347..4dd6938728 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 @@ -5,11 +5,8 @@ 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.*; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; @@ -83,7 +80,6 @@ public ReconcileResult reconcile(ConfigMap primary, Context c public Class resourceType() { return Object.class; } - } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java deleted file mode 100644 index 520f44365f..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java +++ /dev/null @@ -1,149 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.external; - -import java.util.Optional; -import java.util.function.Supplier; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.processing.dependent.Creator; -import io.javaoperatorsdk.operator.processing.dependent.Updater; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; -import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@SuppressWarnings("unchecked") -class AbstractSimpleDependentResourceTest { - - UpdatableCache updatableCacheMock = mock(UpdatableCache.class); - Supplier supplierMock = mock(Supplier.class); - - SimpleDependentResource simpleDependentResource = - new SimpleDependentResource(updatableCacheMock, supplierMock); - - @BeforeEach - void setup() { - when(supplierMock.get()).thenReturn(SampleExternalResource.testResource1()); - } - - @Test - void getsTheResourceFromSupplyIfReconciling() { - simpleDependentResource = new SimpleDependentResource(supplierMock); - - simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); - - verify(supplierMock, times(1)).get(); - assertThat(simpleDependentResource.getSecondaryResource(TestUtils.testCustomResource1(), null)) - .isPresent() - .isEqualTo(Optional.of(SampleExternalResource.testResource1())); - } - - @Test - void getResourceReadsTheResourceFromCache() { - simpleDependentResource.getSecondaryResource(TestUtils.testCustomResource1(), null); - - verify(supplierMock, times(0)).get(); - verify(updatableCacheMock, times(1)).get(any()); - } - - @Test - void createPutsNewResourceToTheCache() { - when(supplierMock.get()).thenReturn(null); - when(updatableCacheMock.get(any())).thenReturn(Optional.empty()); - - simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); - - verify(updatableCacheMock, times(1)).put(any(), any()); - } - - @Test - void updatePutsNewResourceToCache() { - var actual = SampleExternalResource.testResource1(); - actual.setValue("changedValue"); - when(supplierMock.get()).thenReturn(actual); - when(updatableCacheMock.get(any())).thenReturn(Optional.of(actual)); - - simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); - - verify(updatableCacheMock, times(1)) - .put(ResourceID.fromResource(TestUtils.testCustomResource1()), actual); - - verify(updatableCacheMock, times(1)) - .put( - ResourceID.fromResource(TestUtils.testCustomResource1()), - SampleExternalResource.testResource1()); - } - - @Test - void deleteRemovesResourceFromCache() { - simpleDependentResource.delete(TestUtils.testCustomResource1(), null); - verify(updatableCacheMock, times(1)).remove(any()); - } - - private static class SimpleDependentResource - extends AbstractSimpleDependentResource - implements Creator, - Updater, - Deleter { - - private final Supplier supplier; - - public SimpleDependentResource(Supplier supplier) { - this.supplier = supplier; - } - - public SimpleDependentResource( - UpdatableCache cache, Supplier supplier) { - super(cache); - this.supplier = supplier; - } - - @Override - public Optional fetchResource(HasMetadata primaryResource) { - return Optional.ofNullable(supplier.get()); - } - - @Override - protected void deleteResource(TestCustomResource primary, - Context context) {} - - @Override - public SampleExternalResource create( - SampleExternalResource desired, TestCustomResource primary, - Context context) { - return SampleExternalResource.testResource1(); - } - - @Override - public SampleExternalResource update( - SampleExternalResource actual, - SampleExternalResource desired, - TestCustomResource primary, - Context context) { - return SampleExternalResource.testResource1(); - } - - @Override - protected SampleExternalResource desired(TestCustomResource primary, - Context context) { - return SampleExternalResource.testResource1(); - } - - @Override - public Class resourceType() { - return SampleExternalResource.class; - } - } -} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java index 2332778e2e..e3b97afb3c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java @@ -11,7 +11,8 @@ public class ManagedWorkflowTestUtils { @SuppressWarnings("unchecked") public static DependentResourceSpec createDRS(String name, String... dependOns) { return new DependentResourceSpec(EmptyTestDependentResource.class, - null, name, Set.of(dependOns), null, null, null); + null, name, Set.of(dependOns), null, null, null, + null); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestConstants.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestConstants.java new file mode 100644 index 0000000000..1191e6a121 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestConstants.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator; + +public class IntegrationTestConstants { + + public static final int GARBAGE_COLLECTION_TIMEOUT_SECONDS = 30; + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesDependentGarbageCollectionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesDependentGarbageCollectionIT.java index dc3f080ab7..c32f328d76 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesDependentGarbageCollectionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesDependentGarbageCollectionIT.java @@ -43,10 +43,11 @@ void resourceSecondaryResourceIsGarbageCollected() { operator.delete(createdResources); - await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> { - ConfigMap cm = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); - assertThat(cm).isNull(); - }); + await().atMost(Duration.ofSeconds(IntegrationTestConstants.GARBAGE_COLLECTION_TIMEOUT_SECONDS)) + .untilAsserted(() -> { + ConfigMap cm = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(cm).isNull(); + }); } @Test diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleManagedDependentSameTypeIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleManagedDependentSameTypeIT.java new file mode 100644 index 0000000000..32fe0eaec4 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleManagedDependentSameTypeIT.java @@ -0,0 +1,80 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceCustomResource; +import io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceReconciler; +import io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceSpec; + +import static io.javaoperatorsdk.operator.IntegrationTestConstants.GARBAGE_COLLECTION_TIMEOUT_SECONDS; +import static io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceReconciler.DATA_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class MultipleManagedDependentSameTypeIT { + + public static final String TEST_RESOURCE_NAME = "test1"; + public static final String DEFAULT_SPEC_VALUE = "val"; + public static final String UPDATED_SPEC_VALUE = "updated-val"; + public static final int SECONDS = 30; + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler(new MultipleManagedDependentResourceReconciler()) + .build(); + + + @Test + void handlesCrudOperations() { + operator.create(testResource()); + assertConfigMapsPresent(DEFAULT_SPEC_VALUE); + + var updatedResource = testResource(); + updatedResource.getSpec().setValue(UPDATED_SPEC_VALUE); + operator.replace(updatedResource); + assertConfigMapsPresent(UPDATED_SPEC_VALUE); + + operator.delete(testResource()); + assertConfigMapsDeleted(); + } + + private void assertConfigMapsPresent(String expectedData) { + await().untilAsserted(() -> { + var maps = operator.getKubernetesClient().configMaps() + .inNamespace(operator.getNamespace()).list().getItems().stream() + .filter(cm -> cm.getMetadata().getName().startsWith(TEST_RESOURCE_NAME)) + .collect(Collectors.toList()); + assertThat(maps).hasSize(2); + assertThat(maps).allMatch(cm -> cm.getData().get(DATA_KEY).equals(expectedData)); + }); + } + + private void assertConfigMapsDeleted() { + await().atMost(Duration.ofSeconds(GARBAGE_COLLECTION_TIMEOUT_SECONDS)).untilAsserted(() -> { + var maps = operator.getKubernetesClient().configMaps() + .inNamespace(operator.getNamespace()).list().getItems().stream() + .filter(cm -> cm.getMetadata().getName().startsWith(TEST_RESOURCE_NAME)) + .collect(Collectors.toList()); + assertThat(maps).hasSize(0); + }); + } + + private MultipleManagedDependentResourceCustomResource testResource() { + var res = new MultipleManagedDependentResourceCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .build()); + + res.setSpec(new MultipleManagedDependentResourceSpec()); + res.getSpec().setValue(DEFAULT_SPEC_VALUE); + return res; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleManagedExternalDependentSameTypeIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleManagedExternalDependentSameTypeIT.java new file mode 100644 index 0000000000..ec2b7a394a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleManagedExternalDependentSameTypeIT.java @@ -0,0 +1,71 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceSpec; +import io.javaoperatorsdk.operator.sample.multiplemanagedexternaldependenttype.MultipleManagedExternalDependentResourceCustomResource; +import io.javaoperatorsdk.operator.sample.multiplemanagedexternaldependenttype.MultipleManagedExternalDependentResourceReconciler; +import io.javaoperatorsdk.operator.support.ExternalServiceMock; + +import static io.javaoperatorsdk.operator.MultipleManagedDependentSameTypeIT.DEFAULT_SPEC_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class MultipleManagedExternalDependentSameTypeIT { + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler(new MultipleManagedExternalDependentResourceReconciler()) + .build(); + + public static final String TEST_RESOURCE_NAME = "test1"; + public static final String DEFAULT_SPEC_VALUE = "val"; + public static final String UPDATED_SPEC_VALUE = "updated-val"; + + protected ExternalServiceMock externalServiceMock = ExternalServiceMock.getInstance(); + + @Test + void handlesExternalCrudOperations() { + operator.create(testResource()); + assertResourceCreatedWithData(DEFAULT_SPEC_VALUE); + + var updatedResource = testResource(); + updatedResource.getSpec().setValue(UPDATED_SPEC_VALUE); + operator.replace(updatedResource); + assertResourceCreatedWithData(UPDATED_SPEC_VALUE); + + operator.delete(testResource()); + assertExternalResourceDeleted(); + } + + private void assertExternalResourceDeleted() { + await().untilAsserted(() -> { + var resources = externalServiceMock.listResources(); + assertThat(resources).hasSize(0); + }); + } + + private void assertResourceCreatedWithData(String expectedData) { + await().untilAsserted(() -> { + var resources = externalServiceMock.listResources(); + assertThat(resources).hasSize(2); + assertThat(resources).allMatch(er -> er.getData().equals(expectedData)); + }); + } + + private MultipleManagedExternalDependentResourceCustomResource testResource() { + var res = new MultipleManagedExternalDependentResourceCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .build()); + + res.setSpec(new MultipleManagedDependentResourceSpec()); + res.getSpec().setValue(DEFAULT_SPEC_VALUE); + return res; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkDependentDeleterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkDependentDeleterIT.java new file mode 100644 index 0000000000..a934bdd1f3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkDependentDeleterIT.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.bulkdependent; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.bulkdependent.ManagedDeleterBulkReconciler; + +public class BulkDependentDeleterIT extends BulkDependentTestBase { + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder().withReconciler(new ManagedDeleterBulkReconciler()) + .build(); + + @Override + LocallyRunOperatorExtension extension() { + return extension; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkDependentTestBase.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkDependentTestBase.java new file mode 100644 index 0000000000..605731623c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkDependentTestBase.java @@ -0,0 +1,114 @@ +package io.javaoperatorsdk.operator.bulkdependent; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestSpec; +import io.javaoperatorsdk.operator.sample.bulkdependent.ConfigMapDeleterBulkDependentResource; + +import static io.javaoperatorsdk.operator.sample.bulkdependent.ConfigMapDeleterBulkDependentResource.LABEL_KEY; +import static io.javaoperatorsdk.operator.sample.bulkdependent.ConfigMapDeleterBulkDependentResource.LABEL_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public abstract class BulkDependentTestBase { + + public static final String TEST_RESOURCE_NAME = "test"; + public static final int INITIAL_NUMBER_OF_CONFIG_MAPS = 3; + public static final String INITIAL_ADDITIONAL_DATA = "initialData"; + public static final String NEW_VERSION_OF_ADDITIONAL_DATA = "newVersionOfAdditionalData"; + + @Test + public void managesBulkConfigMaps() { + extension().create(testResource()); + assertNumberOfConfigMaps(3); + + updateSpecWithNumber(1); + assertNumberOfConfigMaps(1); + + updateSpecWithNumber(5); + assertNumberOfConfigMaps(5); + + extension().delete(testResource()); + assertNumberOfConfigMaps(0); + } + + @Test + public void updatesData() { + extension().create(testResource()); + assertNumberOfConfigMaps(3); + assertAdditionalDataOnConfigMaps(INITIAL_ADDITIONAL_DATA); + + updateSpecWithNewAdditionalData(NEW_VERSION_OF_ADDITIONAL_DATA); + assertAdditionalDataOnConfigMaps(NEW_VERSION_OF_ADDITIONAL_DATA); + } + + private void assertNumberOfConfigMaps(int n) { + // this test was failing with a lower timeout on GitHub, probably the garbage collection was + // slower there. + await().atMost(Duration.ofSeconds(30)) + .untilAsserted(() -> { + var cms = + extension().getKubernetesClient().configMaps().inNamespace(extension().getNamespace()) + .withLabel(LABEL_KEY, LABEL_VALUE) + .list().getItems(); + assertThat(cms).withFailMessage("Number of items is still: " + cms.size()) + .hasSize(n); + }); + } + + private void assertAdditionalDataOnConfigMaps(String expectedValue) { + await().atMost(Duration.ofSeconds(30)) + .untilAsserted(() -> { + var cms = + extension().getKubernetesClient().configMaps().inNamespace(extension().getNamespace()) + .withLabel(LABEL_KEY, LABEL_VALUE) + .list().getItems(); + cms.forEach(cm -> { + assertThat(cm.getData().get(ConfigMapDeleterBulkDependentResource.ADDITIONAL_DATA_KEY)) + .isEqualTo(expectedValue); + }); + }); + } + + public static BulkDependentTestCustomResource testResource() { + BulkDependentTestCustomResource cr = new BulkDependentTestCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName(TEST_RESOURCE_NAME); + cr.setSpec(new BulkDependentTestSpec()); + cr.getSpec().setNumberOfResources(INITIAL_NUMBER_OF_CONFIG_MAPS); + cr.getSpec().setAdditionalData(INITIAL_ADDITIONAL_DATA); + return cr; + } + + private void updateSpecWithNewAdditionalData(String data) { + var resource = testResource(); + resource.getSpec().setAdditionalData(data); + extension().replace(resource); + } + + public static void updateSpecWithNewAdditionalData(LocallyRunOperatorExtension extension, + String data) { + var resource = testResource(); + resource.getSpec().setAdditionalData(data); + extension.replace(resource); + } + + private void updateSpecWithNumber(int n) { + var resource = testResource(); + resource.getSpec().setNumberOfResources(n); + extension().replace(resource); + } + + public static void updateSpecWithNumber(LocallyRunOperatorExtension extension, int n) { + var resource = testResource(); + resource.getSpec().setNumberOfResources(n); + extension.replace(resource); + } + + abstract LocallyRunOperatorExtension extension(); +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkExternalDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkExternalDependentIT.java new file mode 100644 index 0000000000..29f66e8205 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/BulkExternalDependentIT.java @@ -0,0 +1,56 @@ +package io.javaoperatorsdk.operator.bulkdependent; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.bulkdependent.external.ExternalBulkResourceReconciler; +import io.javaoperatorsdk.operator.sample.bulkdependent.external.ExternalServiceMock; + +import static io.javaoperatorsdk.operator.bulkdependent.BulkDependentTestBase.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class BulkExternalDependentIT { + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder().withReconciler(new ExternalBulkResourceReconciler()) + .build(); + + ExternalServiceMock externalServiceMock = ExternalServiceMock.getInstance(); + + @Test + void managesExternalBulkResources() { + extension.create(testResource()); + assertResourceNumberAndData(3, INITIAL_ADDITIONAL_DATA); + + updateSpecWithNumber(extension, 1); + assertResourceNumberAndData(1, INITIAL_ADDITIONAL_DATA); + + updateSpecWithNumber(extension, 5); + assertResourceNumberAndData(5, INITIAL_ADDITIONAL_DATA); + + extension.delete(testResource()); + assertResourceNumberAndData(0, INITIAL_ADDITIONAL_DATA); + } + + + @Test + void handlesResourceUpdates() { + extension.create(testResource()); + assertResourceNumberAndData(3, INITIAL_ADDITIONAL_DATA); + + updateSpecWithNewAdditionalData(extension, NEW_VERSION_OF_ADDITIONAL_DATA); + assertResourceNumberAndData(3, NEW_VERSION_OF_ADDITIONAL_DATA); + } + + private void assertResourceNumberAndData(int n, String data) { + await().untilAsserted(() -> { + var resources = externalServiceMock.listResources(); + assertThat(resources).hasSize(n); + assertThat(resources).allMatch(r -> r.getData().equals(data)); + }); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/ManagedBulkDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/ManagedBulkDependentIT.java new file mode 100644 index 0000000000..7f074ac8f5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/ManagedBulkDependentIT.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.bulkdependent; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.bulkdependent.ManagedBulkDependentReconciler; + +class ManagedBulkDependentIT extends BulkDependentTestBase { + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder().withReconciler(new ManagedBulkDependentReconciler()) + .build(); + + + @Override + LocallyRunOperatorExtension extension() { + return extension; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/StandaloneBulkDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/StandaloneBulkDependentIT.java new file mode 100644 index 0000000000..683cc1662b --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/bulkdependent/StandaloneBulkDependentIT.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.bulkdependent; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.bulkdependent.StandaloneBulkDependentReconciler; + +class StandaloneBulkDependentIT extends BulkDependentTestBase { + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder().withReconciler(new StandaloneBulkDependentReconciler()) + .build(); + + @Override + LocallyRunOperatorExtension extension() { + return extension; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/BulkDependentTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/BulkDependentTestCustomResource.java new file mode 100644 index 0000000000..68e6297f8c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/BulkDependentTestCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("sbd") +public class BulkDependentTestCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/BulkDependentTestSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/BulkDependentTestSpec.java new file mode 100644 index 0000000000..5266950b41 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/BulkDependentTestSpec.java @@ -0,0 +1,25 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent; + +public class BulkDependentTestSpec { + + private Integer numberOfResources; + private String additionalData; + + public Integer getNumberOfResources() { + return numberOfResources; + } + + public BulkDependentTestSpec setNumberOfResources(Integer numberOfResources) { + this.numberOfResources = numberOfResources; + return this; + } + + public BulkDependentTestSpec setAdditionalData(String additionalData) { + this.additionalData = additionalData; + return this; + } + + public String getAdditionalData() { + return additionalData; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/CRUDConfigMapBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/CRUDConfigMapBulkDependentResource.java new file mode 100644 index 0000000000..83cec0bb69 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/CRUDConfigMapBulkDependentResource.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent; + +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; + +public class CRUDConfigMapBulkDependentResource extends ConfigMapDeleterBulkDependentResource + implements GarbageCollected { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ConfigMapDeleterBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ConfigMapDeleterBulkDependentResource.java new file mode 100644 index 0000000000..a7fbd9cb98 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ConfigMapDeleterBulkDependentResource.java @@ -0,0 +1,72 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; + +/** + * Not using CRUDKubernetesDependentResource so the delete functionality can be tested. + */ +public class ConfigMapDeleterBulkDependentResource + extends + KubernetesDependentResource + implements Creator, + Updater, + Deleter, + BulkDependentResource { + + public static final String LABEL_KEY = "bulk"; + public static final String LABEL_VALUE = "true"; + public static final String ADDITIONAL_DATA_KEY = "additionalData"; + + public ConfigMapDeleterBulkDependentResource() { + super(ConfigMap.class); + } + + @Override + public ConfigMap desired(BulkDependentTestCustomResource primary, + int index, Context context) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName() + "-" + index) + .withNamespace(primary.getMetadata().getNamespace()) + .withLabels(Map.of(LABEL_KEY, LABEL_VALUE)) + .build()); + configMap.setData( + Map.of("number", "" + index, ADDITIONAL_DATA_KEY, primary.getSpec().getAdditionalData())); + return configMap; + } + + @Override + public int count(BulkDependentTestCustomResource primary, + Context context) { + return primary.getSpec().getNumberOfResources(); + } + + @Override + public ResourceDiscriminator getResourceDiscriminator( + int index) { + return (resource, primary, context) -> { + var resources = context.getSecondaryResources(resource).stream() + .filter(r -> r.getMetadata().getName().endsWith("-" + index)) + .collect(Collectors.toList()); + if (resources.isEmpty()) { + return Optional.empty(); + } else if (resources.size() > 1) { + throw new IllegalStateException("More than one resource found for index:" + index); + } else { + return Optional.of(resources.get(0)); + } + }; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedBulkDependentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedBulkDependentReconciler.java new file mode 100644 index 0000000000..3b2acd942e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedBulkDependentReconciler.java @@ -0,0 +1,25 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@ControllerConfiguration(dependents = @Dependent(type = CRUDConfigMapBulkDependentResource.class)) +public class ManagedBulkDependentReconciler + implements Reconciler { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + BulkDependentTestCustomResource resource, + Context context) throws Exception { + + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedDeleterBulkReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedDeleterBulkReconciler.java new file mode 100644 index 0000000000..e759bdd200 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedDeleterBulkReconciler.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@ControllerConfiguration( + dependents = @Dependent(type = ConfigMapDeleterBulkDependentResource.class)) +public class ManagedDeleterBulkReconciler implements Reconciler { + @Override + public UpdateControl reconcile( + BulkDependentTestCustomResource resource, + Context context) + throws Exception { + + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/StandaloneBulkDependentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/StandaloneBulkDependentReconciler.java new file mode 100644 index 0000000000..4033583340 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/StandaloneBulkDependentReconciler.java @@ -0,0 +1,58 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration +public class StandaloneBulkDependentReconciler + implements Reconciler, TestExecutionInfoProvider, + EventSourceInitializer, KubernetesClientAware { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + private ConfigMapDeleterBulkDependentResource dependent; + private KubernetesClient kubernetesClient; + + public StandaloneBulkDependentReconciler() { + dependent = new CRUDConfigMapBulkDependentResource(); + } + + @Override + public UpdateControl reconcile( + BulkDependentTestCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + + dependent.reconcile(resource, context); + + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + return EventSourceInitializer + .nameEventSources(dependent.initEventSource(context)); + } + + @Override + public KubernetesClient getKubernetesClient() { + return kubernetesClient; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + dependent.setKubernetesClient(kubernetesClient); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkDependentResource.java new file mode 100644 index 0000000000..110626a923 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkDependentResource.java @@ -0,0 +1,101 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent.external; + +import java.util.*; +import java.util.stream.Collectors; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.processing.dependent.*; +import io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource; + +public class ExternalBulkDependentResource + extends PollingDependentResource + implements BulkDependentResource, + BulkUpdater { + + public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#"; + + private final ExternalServiceMock externalServiceMock = ExternalServiceMock.getInstance(); + + public ExternalBulkDependentResource() { + super(ExternalResource.class, ExternalResource::getId); + } + + @Override + public Map> fetchResources() { + Map> result = new HashMap<>(); + var resources = externalServiceMock.listResources(); + resources.forEach(er -> { + var resourceID = toResourceID(er); + result.putIfAbsent(resourceID, new HashSet<>()); + result.get(resourceID).add(er); + }); + return result; + } + + @Override + public void delete(BulkDependentTestCustomResource primary, + Context context) { + deleteBulkResourcesIfRequired(0, lastKnownBulkSize(), primary, context); + } + + @Override + public int count(BulkDependentTestCustomResource primary, + Context context) { + return primary.getSpec().getNumberOfResources(); + } + + @Override + public void deleteBulkResourceWithIndex(BulkDependentTestCustomResource primary, + ExternalResource resource, int i, Context context) { + externalServiceMock.delete(resource.getId()); + } + + @Override + public ExternalResource desired(BulkDependentTestCustomResource primary, int index, + Context context) { + return new ExternalResource(toExternalResourceId(primary, index), + primary.getSpec().getAdditionalData()); + } + + @Override + public ExternalResource create(ExternalResource desired, BulkDependentTestCustomResource primary, + Context context) { + return externalServiceMock.create(desired); + } + + @Override + public ExternalResource update(ExternalResource actual, ExternalResource desired, + BulkDependentTestCustomResource primary, Context context) { + return externalServiceMock.update(desired); + } + + @Override + public Matcher.Result match(ExternalResource actualResource, + BulkDependentTestCustomResource primary, + int index, Context context) { + var desired = desired(primary, index, context); + return Matcher.Result.computed(desired.equals(actualResource), desired); + } + + private static String toExternalResourceId(BulkDependentTestCustomResource primary, int i) { + return primary.getMetadata().getName() + EXTERNAL_RESOURCE_NAME_DELIMITER + + primary.getMetadata().getNamespace() + + EXTERNAL_RESOURCE_NAME_DELIMITER + i; + } + + private ResourceID toResourceID(ExternalResource externalResource) { + var parts = externalResource.getId().split(EXTERNAL_RESOURCE_NAME_DELIMITER); + return new ResourceID(parts[0], parts[1]); + } + + @Override + public ResourceDiscriminator getResourceDiscriminator( + int index) { + return (resource, primary, context) -> context.getSecondaryResources(resource).stream() + .filter(r -> r.getId().endsWith(EXTERNAL_RESOURCE_NAME_DELIMITER + index)) + .collect(Collectors.toList()).stream().findFirst(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkResourceReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkResourceReconciler.java new file mode 100644 index 0000000000..2543422d74 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkResourceReconciler.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent.external; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource; + +@ControllerConfiguration(dependents = @Dependent(type = ExternalBulkDependentResource.class)) +public class ExternalBulkResourceReconciler implements Reconciler { + + @Override + public UpdateControl reconcile( + BulkDependentTestCustomResource resource, Context context) + throws Exception { + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalResource.java new file mode 100644 index 0000000000..935fd99e47 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalResource.java @@ -0,0 +1,37 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent.external; + +import java.util.Objects; + +public class ExternalResource { + + private String id; + private String data; + + public ExternalResource(String id, String data) { + this.id = id; + this.data = data; + } + + public String getId() { + return id; + } + + public String getData() { + return data; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ExternalResource that = (ExternalResource) o; + return Objects.equals(id, that.id) && Objects.equals(data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(id, data); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalServiceMock.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalServiceMock.java new file mode 100644 index 0000000000..e73062ccf2 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalServiceMock.java @@ -0,0 +1,39 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent.external; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class ExternalServiceMock { + + private static ExternalServiceMock serviceMock = new ExternalServiceMock(); + + private Map resourceMap = new ConcurrentHashMap<>(); + + public ExternalResource create(ExternalResource externalResource) { + resourceMap.put(externalResource.getId(), externalResource); + return externalResource; + } + + public Optional read(String id) { + return Optional.ofNullable(resourceMap.get(id)); + } + + public ExternalResource update(ExternalResource externalResource) { + return resourceMap.put(externalResource.getId(), externalResource); + } + + public Optional delete(String id) { + return Optional.ofNullable(resourceMap.remove(id)); + } + + public List listResources() { + return new ArrayList<>(resourceMap.values()); + } + + public static ExternalServiceMock getInstance() { + return serviceMock; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/kubernetesdependentgarbagecollection/DependentGarbageCollectionTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/kubernetesdependentgarbagecollection/DependentGarbageCollectionTestReconciler.java index e9b947a83b..c20d573a03 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/kubernetesdependentgarbagecollection/DependentGarbageCollectionTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/kubernetesdependentgarbagecollection/DependentGarbageCollectionTestReconciler.java @@ -32,8 +32,8 @@ public DependentGarbageCollectionTestReconciler() { @Override public Map prepareEventSources( EventSourceContext context) { - return EventSourceInitializer - .nameEventSources(configMapDependent.initEventSource(context)); + return EventSourceInitializer.nameEventSourcesFromDependentResource(context, + configMapDependent); } @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentresource/MultipleDependentResourceConfigMap.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentresource/MultipleDependentResourceConfigMap.java index 1adbfb9f95..5c2e9974b5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentresource/MultipleDependentResourceConfigMap.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentresource/MultipleDependentResourceConfigMap.java @@ -7,9 +7,7 @@ import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; -@KubernetesDependent public class MultipleDependentResourceConfigMap extends CRUDKubernetesDependentResource { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/ConfigMap1Discriminator.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/ConfigMap1Discriminator.java new file mode 100644 index 0000000000..cc20dfa45e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/ConfigMap1Discriminator.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +import static io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceConfigMap1.NAME_SUFFIX; + +public class ConfigMap1Discriminator + implements ResourceDiscriminator { + @Override + public Optional distinguish(Class resource, + MultipleManagedDependentResourceCustomResource primary, + Context context) { + InformerEventSource ies = + (InformerEventSource) context + .eventSourceRetriever().getResourceEventSourceFor(ConfigMap.class); + + return ies.get(new ResourceID(primary.getMetadata().getName() + NAME_SUFFIX, + primary.getMetadata().getNamespace())); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/ConfigMap2Discriminator.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/ConfigMap2Discriminator.java new file mode 100644 index 0000000000..8bda6afcee --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/ConfigMap2Discriminator.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +import static io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceConfigMap2.NAME_SUFFIX; + +public class ConfigMap2Discriminator + implements ResourceDiscriminator { + @Override + public Optional distinguish(Class resource, + MultipleManagedDependentResourceCustomResource primary, + Context context) { + InformerEventSource ies = + (InformerEventSource) context + .eventSourceRetriever().getResourceEventSourceFor(ConfigMap.class); + + return ies.get(new ResourceID(primary.getMetadata().getName() + NAME_SUFFIX, + primary.getMetadata().getNamespace())); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap1.java new file mode 100644 index 0000000000..0d450c1871 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap1.java @@ -0,0 +1,40 @@ +package io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +import static io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceReconciler.CONFIG_MAP_EVENT_SOURCE; + +@KubernetesDependent(eventSourceToUse = CONFIG_MAP_EVENT_SOURCE, + resourceDiscriminator = ConfigMap1Discriminator.class) +public class MultipleManagedDependentResourceConfigMap1 + extends + CRUDKubernetesDependentResource { + + public static final String NAME_SUFFIX = "-1"; + + public MultipleManagedDependentResourceConfigMap1() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(MultipleManagedDependentResourceCustomResource primary, + Context context) { + Map data = new HashMap<>(); + data.put(MultipleManagedDependentResourceReconciler.DATA_KEY, primary.getSpec().getValue()); + + return new ConfigMapBuilder() + .withNewMetadata() + .withName(primary.getMetadata().getName() + NAME_SUFFIX) + .withNamespace(primary.getMetadata().getNamespace()) + .endMetadata() + .withData(data) + .build(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap2.java new file mode 100644 index 0000000000..11902fc518 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceConfigMap2.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +import static io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceReconciler.CONFIG_MAP_EVENT_SOURCE; +import static io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceReconciler.DATA_KEY; + +@KubernetesDependent(eventSourceToUse = CONFIG_MAP_EVENT_SOURCE, + resourceDiscriminator = ConfigMap2Discriminator.class) +public class MultipleManagedDependentResourceConfigMap2 + extends + CRUDKubernetesDependentResource { + + public static final String NAME_SUFFIX = "-2"; + + public MultipleManagedDependentResourceConfigMap2() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(MultipleManagedDependentResourceCustomResource primary, + Context context) { + Map data = new HashMap<>(); + data.put(DATA_KEY, primary.getSpec().getValue()); + + return new ConfigMapBuilder() + .withNewMetadata() + .withName(primary.getMetadata().getName() + NAME_SUFFIX) + .withNamespace(primary.getMetadata().getNamespace()) + .endMetadata() + .withData(data) + .build(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceCustomResource.java new file mode 100644 index 0000000000..44564727a2 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceCustomResource.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("mmd") +public class MultipleManagedDependentResourceCustomResource + extends CustomResource + implements Namespaced { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceReconciler.java new file mode 100644 index 0000000000..edfa7ad2bd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceReconciler.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration(dependents = { + @Dependent(type = MultipleManagedDependentResourceConfigMap1.class), + @Dependent(type = MultipleManagedDependentResourceConfigMap2.class) +}) +public class MultipleManagedDependentResourceReconciler + implements Reconciler, + TestExecutionInfoProvider, + EventSourceInitializer { + + public static final String CONFIG_MAP_EVENT_SOURCE = "ConfigMapEventSource"; + public static final String DATA_KEY = "key"; + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + public MultipleManagedDependentResourceReconciler() {} + + @Override + public UpdateControl reconcile( + MultipleManagedDependentResourceCustomResource resource, + Context context) { + numberOfExecutions.getAndIncrement(); + + return UpdateControl.noUpdate(); + } + + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + InformerEventSource ies = + new InformerEventSource<>(InformerConfiguration.from(ConfigMap.class, context) + .build(), context); + + return Map.of(CONFIG_MAP_EVENT_SOURCE, ies); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceSpec.java new file mode 100644 index 0000000000..cdd524e03e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanageddependentsametype/MultipleManagedDependentResourceSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype; + +public class MultipleManagedDependentResourceSpec { + + private String value; + + public String getValue() { + return value; + } + + public MultipleManagedDependentResourceSpec setValue(String value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/AbstractExternalDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/AbstractExternalDependentResource.java new file mode 100644 index 0000000000..f7d3c91dd3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/AbstractExternalDependentResource.java @@ -0,0 +1,75 @@ +package io.javaoperatorsdk.operator.sample.multiplemanagedexternaldependenttype; + +import java.util.Map; +import java.util.Set; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Matcher; +import io.javaoperatorsdk.operator.processing.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.support.ExternalResource; +import io.javaoperatorsdk.operator.support.ExternalServiceMock; + +public abstract class AbstractExternalDependentResource extends + PollingDependentResource + implements Creator, + Updater, + Deleter { + + protected ExternalServiceMock externalServiceMock = ExternalServiceMock.getInstance(); + + public AbstractExternalDependentResource() { + super(ExternalResource.class, ExternalResource::getId); + } + + @Override + public Map> fetchResources() { + throw new IllegalStateException("Should not be called"); + } + + @Override + public ExternalResource create(ExternalResource desired, + MultipleManagedExternalDependentResourceCustomResource primary, + Context context) { + return externalServiceMock.create(desired); + } + + @Override + public ExternalResource update(ExternalResource actual, + ExternalResource desired, + MultipleManagedExternalDependentResourceCustomResource primary, + Context context) { + return externalServiceMock.update(desired); + } + + @Override + public Matcher.Result match(ExternalResource actualResource, + MultipleManagedExternalDependentResourceCustomResource primary, + Context context) { + var desired = desired(primary, context); + return Matcher.Result.computed(actualResource.equals(desired), desired); + } + + @Override + public void delete(MultipleManagedExternalDependentResourceCustomResource primary, + Context context) { + externalServiceMock.delete(toExternalResourceID(primary)); + } + + protected ExternalResource desired(MultipleManagedExternalDependentResourceCustomResource primary, + Context context) { + return new ExternalResource(toExternalResourceID(primary), + primary.getSpec().getValue()); + } + + protected String toExternalResourceID( + MultipleManagedExternalDependentResourceCustomResource primary) { + return ExternalResource.toExternalResourceId(primary) + resourceIDSuffix(); + } + + protected abstract String resourceIDSuffix(); + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalDependentResource1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalDependentResource1.java new file mode 100644 index 0000000000..cfe67a3796 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalDependentResource1.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.multiplemanagedexternaldependenttype; + +public class ExternalDependentResource1 extends AbstractExternalDependentResource { + + public static final String SUFFIX = "-1"; + + public ExternalDependentResource1() { + setResourceDiscriminator(new ExternalResourceDiscriminator(SUFFIX)); + } + + @Override + protected String resourceIDSuffix() { + return SUFFIX; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalDependentResource2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalDependentResource2.java new file mode 100644 index 0000000000..29bb237e1a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalDependentResource2.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.multiplemanagedexternaldependenttype; + +public class ExternalDependentResource2 extends AbstractExternalDependentResource { + + public static final String SUFFIX = "-2"; + + public ExternalDependentResource2() { + setResourceDiscriminator(new ExternalResourceDiscriminator(SUFFIX)); + } + + @Override + protected String resourceIDSuffix() { + return SUFFIX; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalResourceDiscriminator.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalResourceDiscriminator.java new file mode 100644 index 0000000000..5a394113c1 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/ExternalResourceDiscriminator.java @@ -0,0 +1,25 @@ +package io.javaoperatorsdk.operator.sample.multiplemanagedexternaldependenttype; + +import java.util.Optional; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.support.ExternalResource; + +public class ExternalResourceDiscriminator implements + ResourceDiscriminator { + + private final String suffix; + + public ExternalResourceDiscriminator(String suffix) { + this.suffix = suffix; + } + + @Override + public Optional distinguish(Class resource, + MultipleManagedExternalDependentResourceCustomResource primary, + Context context) { + var resources = context.getSecondaryResources(ExternalResource.class); + return resources.stream().filter(r -> r.getId().endsWith(suffix)).findFirst(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentResourceCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentResourceCustomResource.java new file mode 100644 index 0000000000..c989a5c96c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentResourceCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.multiplemanagedexternaldependenttype; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; +import io.javaoperatorsdk.operator.sample.multiplemanageddependentsametype.MultipleManagedDependentResourceSpec; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("mme") +public class MultipleManagedExternalDependentResourceCustomResource + extends CustomResource + implements Namespaced { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentResourceReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentResourceReconciler.java new file mode 100644 index 0000000000..28b94ac715 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentResourceReconciler.java @@ -0,0 +1,65 @@ +package io.javaoperatorsdk.operator.sample.multiplemanagedexternaldependenttype; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.polling.PollingEventSource; +import io.javaoperatorsdk.operator.support.ExternalResource; +import io.javaoperatorsdk.operator.support.ExternalServiceMock; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration(dependents = { + @Dependent(type = ExternalDependentResource1.class, + eventSource = MultipleManagedExternalDependentResourceReconciler.CONFIG_MAP_EVENT_SOURCE), + @Dependent(type = ExternalDependentResource2.class, + eventSource = MultipleManagedExternalDependentResourceReconciler.CONFIG_MAP_EVENT_SOURCE) +}) +public class MultipleManagedExternalDependentResourceReconciler + implements Reconciler, + TestExecutionInfoProvider, + EventSourceInitializer { + + public static final String CONFIG_MAP_EVENT_SOURCE = "ConfigMapEventSource"; + protected ExternalServiceMock externalServiceMock = ExternalServiceMock.getInstance(); + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + public MultipleManagedExternalDependentResourceReconciler() {} + + @Override + public UpdateControl reconcile( + MultipleManagedExternalDependentResourceCustomResource resource, + Context context) { + numberOfExecutions.getAndIncrement(); + + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + + final var pollingEventSource = new PollingEventSource<>(() -> { + var lists = externalServiceMock.listResources(); + Map> res = new HashMap<>(); + lists.forEach(er -> { + var resourceId = er.toResourceID(); + res.computeIfAbsent(resourceId, rid -> new HashSet<>()); + res.get(resourceId).add(er); + }); + return res; + }, 1000L, ExternalResource.class, ExternalResource::getId); + + return Map.of(CONFIG_MAP_EVENT_SOURCE, pollingEventSource); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primaryindexer/DependentPrimaryIndexerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primaryindexer/DependentPrimaryIndexerTestReconciler.java index 59b6594846..6d79d4ee56 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primaryindexer/DependentPrimaryIndexerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primaryindexer/DependentPrimaryIndexerTestReconciler.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.sample.primaryindexer; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -10,8 +11,8 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; @ControllerConfiguration(dependents = @Dependent( @@ -38,11 +39,11 @@ public Set toPrimaryResourceIDs(ConfigMap dependentResource) { } @Override - public EventSource initEventSource( + public Optional> eventSource( EventSourceContext context) { cache = context.getPrimaryCache(); cache.addIndexer(CONFIG_MAP_RELATION_INDEXER, indexer); - return super.initEventSource(context); + return super.eventSource(context); } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 2dace670ba..70b119c2ab 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -38,8 +38,8 @@ public StandaloneDependentTestReconciler() { @Override public Map prepareEventSources( EventSourceContext context) { - return EventSourceInitializer - .nameEventSources(deploymentDependent.initEventSource(context)); + return EventSourceInitializer.nameEventSourcesFromDependentResource(context, + deploymentDependent); } @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalResource.java new file mode 100644 index 0000000000..cb8d4b74e5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalResource.java @@ -0,0 +1,58 @@ +package io.javaoperatorsdk.operator.support; + +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class ExternalResource { + + public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#"; + + private String id; + private String data; + + public ExternalResource(String id, String data) { + this.id = id; + this.data = data; + } + + public String getId() { + return id; + } + + public String getData() { + return data; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ExternalResource that = (ExternalResource) o; + return Objects.equals(id, that.id) && Objects.equals(data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(id, data); + } + + public ResourceID toResourceID() { + var parts = getId().split(EXTERNAL_RESOURCE_NAME_DELIMITER); + return new ResourceID(parts[0], parts[1]); + } + + public static String toExternalResourceId(HasMetadata primary, int i) { + return primary.getMetadata().getName() + EXTERNAL_RESOURCE_NAME_DELIMITER + + primary.getMetadata().getNamespace() + + EXTERNAL_RESOURCE_NAME_DELIMITER + i; + } + + public static String toExternalResourceId(HasMetadata primary) { + return primary.getMetadata().getName() + EXTERNAL_RESOURCE_NAME_DELIMITER + + primary.getMetadata().getNamespace(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalServiceMock.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalServiceMock.java new file mode 100644 index 0000000000..eea26637fc --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalServiceMock.java @@ -0,0 +1,39 @@ +package io.javaoperatorsdk.operator.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class ExternalServiceMock { + + private static ExternalServiceMock serviceMock = new ExternalServiceMock(); + + private Map resourceMap = new ConcurrentHashMap<>(); + + public ExternalResource create(ExternalResource externalResource) { + resourceMap.put(externalResource.getId(), externalResource); + return externalResource; + } + + public Optional read(String id) { + return Optional.ofNullable(resourceMap.get(id)); + } + + public ExternalResource update(ExternalResource externalResource) { + return resourceMap.put(externalResource.getId(), externalResource); + } + + public Optional delete(String id) { + return Optional.ofNullable(resourceMap.remove(id)); + } + + public List listResources() { + return new ArrayList<>(resourceMap.values()); + } + + public static ExternalServiceMock getInstance() { + return serviceMock; + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index 48e3f37abe..c262822d21 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; @@ -26,8 +25,7 @@ public class SchemaDependentResource extends PerResourcePollingDependentResource - implements EventSourceProvider, - DependentResourceConfigurator, + implements DependentResourceConfigurator, Creator, Deleter { public static final String NAME = "schema"; @@ -97,5 +95,4 @@ public Set fetchResources(MySQLSchema primaryResource) { throw new RuntimeException("Error while trying read Schema", e); } } - } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java index 6b81a9d6e3..0eeba42083 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java @@ -55,9 +55,9 @@ public WebPageDependentsWorkflowReconciler(KubernetesClient kubernetesClient) { @Override public Map prepareEventSources(EventSourceContext context) { - return EventSourceInitializer.nameEventSources(configMapDR.initEventSource(context), - deploymentDR.initEventSource(context), serviceDR.initEventSource(context), - ingressDR.initEventSource(context)); + return EventSourceInitializer.nameEventSourcesFromDependentResource(context, configMapDR, + deploymentDR, serviceDR, + ingressDR); } @Override diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java index b99e130135..15262acb48 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java @@ -40,9 +40,8 @@ public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) @Override public Map prepareEventSources(EventSourceContext context) { - return EventSourceInitializer.nameEventSources(configMapDR.initEventSource(context), - deploymentDR.initEventSource(context), serviceDR.initEventSource(context), - ingressDR.initEventSource(context)); + return EventSourceInitializer.nameEventSourcesFromDependentResource(context, configMapDR, + deploymentDR, serviceDR, ingressDR); } @Override