diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 195c4ec55e..d8e1cfa642 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -699,7 +699,41 @@ leader left off should one of them become elected leader. See sample configuration in the [E2E test](https://github.com/java-operator-sdk/java-operator-sdk/blob/8865302ac0346ee31f2d7b348997ec2913d5922b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java#L21-L23) . -## Monitoring with Micrometer +## Runtime Info + +[RuntimeInfo](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java#L16-L16) +is used mainly to check the actual health of event sources. Based on this information it is easy to implement custom +liveness probes. + +[stopOnInformerErrorDuringStartup](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L168-L168) +setting, where this flag usually needs to be set to false, in order to control the exact liveness properties. + +See also an example implementation in the +[WebPage sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/3e2e7c4c834ef1c409d636156b988125744ca911/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java#L38-L43) + +## Optimization of Caches + +** Cache pruning is an experimental feature. Might a subject of change or even removal in the future. ** + +Operators using informers will initially cache the data for all known resources when starting up +so that access to resources can be performed quickly. Consequently, the memory required for the +operator to run and startup time will both increase quite dramatically when dealing with large +clusters with numerous resources. + +It is thus possible to configure the operator to cache only pruned versions of the resources to +alleviate the memory usage of the primary and secondary caches. This setup, however, has +implications on how reconcilers deal with resources since they will only work with partial +objects. As a consequence, resources need to be updated using PATCH operations only, sending +only required changes. + +To see how to use, and how to handle related caveats regarding how to deal with pruned objects +that leverage +[server side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) patches, +please check the provided +[integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/c688524e64205690ba15587e7ed96a64dc231430/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java) +and associates reconciler. + +Pruned caches are currently not supported with the Dependent Resources feature. ## Automatic Generation of CRDs diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 8378720ed8..c541810f47 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java index fff9cf427f..6bc4f22fdc 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java @@ -5,27 +5,56 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.GroupVersionKind; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Timer; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.CONTROLLER_NAME; + public class MicrometerMetrics implements Metrics { private static final String PREFIX = "operator.sdk."; private static final String RECONCILIATIONS = "reconciliations."; + private static final String RECONCILIATIONS_EXECUTIONS = PREFIX + RECONCILIATIONS + "executions."; + private static final String RECONCILIATIONS_QUEUE_SIZE = PREFIX + RECONCILIATIONS + "queue.size."; private final MeterRegistry registry; + private final Map gauges = new ConcurrentHashMap<>(); public MicrometerMetrics(MeterRegistry registry) { this.registry = registry; } + @Override + public void controllerRegistered(Controller controller) { + String executingThreadsName = + RECONCILIATIONS_EXECUTIONS + controller.getConfiguration().getName(); + AtomicInteger executingThreads = + registry.gauge(executingThreadsName, + gvkTags(controller.getConfiguration().getResourceClass()), + new AtomicInteger(0)); + gauges.put(executingThreadsName, executingThreads); + + String controllerQueueName = + RECONCILIATIONS_QUEUE_SIZE + controller.getConfiguration().getName(); + AtomicInteger controllerQueueSize = + registry.gauge(controllerQueueName, + gvkTags(controller.getConfiguration().getResourceClass()), + new AtomicInteger(0)); + gauges.put(controllerQueueName, controllerQueueSize); + } + public T timeControllerExecution(ControllerExecution execution) { final var name = execution.controllerName(); final var execName = PREFIX + "controllers.execution." + execution.name(); @@ -84,23 +113,45 @@ public void cleanupDoneFor(ResourceID resourceID, Map metadata) } @Override - public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfoNullable, + public void reconcileCustomResource(HasMetadata resource, RetryInfo retryInfoNullable, Map metadata) { Optional retryInfo = Optional.ofNullable(retryInfoNullable); - incrementCounter(resourceID, RECONCILIATIONS + "started", + incrementCounter(ResourceID.fromResource(resource), RECONCILIATIONS + "started", metadata, RECONCILIATIONS + "retries.number", "" + retryInfo.map(RetryInfo::getAttemptCount).orElse(0), RECONCILIATIONS + "retries.last", "" + retryInfo.map(RetryInfo::isLastAttempt).orElse(true)); + + AtomicInteger controllerQueueSize = + gauges.get(RECONCILIATIONS_QUEUE_SIZE + metadata.get(CONTROLLER_NAME)); + controllerQueueSize.incrementAndGet(); } @Override - public void finishedReconciliation(ResourceID resourceID, Map metadata) { - incrementCounter(resourceID, RECONCILIATIONS + "success", metadata); + public void finishedReconciliation(HasMetadata resource, Map metadata) { + incrementCounter(ResourceID.fromResource(resource), RECONCILIATIONS + "success", metadata); } - public void failedReconciliation(ResourceID resourceID, Exception exception, + @Override + public void reconciliationExecutionStarted(HasMetadata resource, Map metadata) { + AtomicInteger reconcilerExecutions = + gauges.get(RECONCILIATIONS_EXECUTIONS + metadata.get(CONTROLLER_NAME)); + reconcilerExecutions.incrementAndGet(); + } + + @Override + public void reconciliationExecutionFinished(HasMetadata resource, Map metadata) { + AtomicInteger reconcilerExecutions = + gauges.get(RECONCILIATIONS_EXECUTIONS + metadata.get(CONTROLLER_NAME)); + reconcilerExecutions.decrementAndGet(); + + AtomicInteger controllerQueueSize = + gauges.get(RECONCILIATIONS_QUEUE_SIZE + metadata.get(CONTROLLER_NAME)); + controllerQueueSize.decrementAndGet(); + } + + public void failedReconciliation(HasMetadata resource, Exception exception, Map metadata) { var cause = exception.getCause(); if (cause == null) { @@ -108,7 +159,8 @@ public void failedReconciliation(ResourceID resourceID, Exception exception, } else if (cause instanceof RuntimeException) { cause = cause.getCause() != null ? cause.getCause() : cause; } - incrementCounter(resourceID, RECONCILIATIONS + "failed", metadata, "exception", + incrementCounter(ResourceID.fromResource(resource), RECONCILIATIONS + "failed", metadata, + "exception", cause.getClass().getSimpleName()); } @@ -116,6 +168,12 @@ public void failedReconciliation(ResourceID resourceID, Exception exception, return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); } + public static List gvkTags(Class resourceClass) { + final var gvk = GroupVersionKind.gvkFor(resourceClass); + return List.of(Tag.of("group", gvk.group), Tag.of("version", gvk.version), + Tag.of("kind", gvk.kind)); + } + private void incrementCounter(ResourceID id, String counterName, Map metadata, String... additionalTags) { final var additionalTagsNb = diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml index 6b22c02bbb..7c435928ae 100644 --- a/operator-framework-bom/pom.xml +++ b/operator-framework-bom/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk operator-framework-bom - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT Operator SDK - Bill of Materials pom Java SDK for implementing Kubernetes operators diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 65de6f8323..e885984058 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java index 37a1a501d0..60808c899e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java @@ -72,6 +72,7 @@ public Operator(KubernetesClient kubernetesClient, ConfigurationService configur } /** Adds a shutdown hook that automatically calls {@link #stop()} when the app shuts down. */ + @Deprecated(forRemoval = true) public void installShutdownHook() { if (!leaderElectionManager.isLeaderElectionEnabled()) { Runtime.getRuntime().addShutdownHook(new Thread(this::stop)); @@ -89,12 +90,11 @@ public KubernetesClient getKubernetesClient() { * where there is no obvious entrypoint to the application which can trigger the injection process * and start the cluster monitoring processes. */ - public void start() { + public synchronized void start() { try { if (started) { return; } - started = true; controllerManager.shouldStart(); final var version = ConfigurationServiceProvider.instance().getVersion(); log.info( @@ -110,6 +110,7 @@ public void start() { // the leader election would start subsequently the processor if on controllerManager.start(!leaderElectionManager.isLeaderElectionEnabled()); leaderElectionManager.start(); + started = true; } catch (Exception e) { log.error("Error starting operator", e); stop(); @@ -216,4 +217,11 @@ public int getRegisteredControllersNumber() { return controllerManager.size(); } + public RuntimeInfo getRuntimeInfo() { + return new RuntimeInfo(this); + } + + boolean isStarted() { + return started; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java index 832c2df6ee..88cd0123b0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RegisteredController.java @@ -3,7 +3,12 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; +import io.javaoperatorsdk.operator.health.ControllerHealthInfo; public interface RegisteredController

extends NamespaceChangeable { + ControllerConfiguration

getConfiguration(); + + ControllerHealthInfo getControllerHealthInfo(); + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java new file mode 100644 index 0000000000..961e519d62 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java @@ -0,0 +1,81 @@ +package io.javaoperatorsdk.operator; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.health.EventSourceHealthIndicator; +import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; + +/** + * RuntimeInfo in general is available when operator is fully started. You can use "isStarted" to + * check that. + */ +@SuppressWarnings("rawtypes") +public class RuntimeInfo { + + private static final Logger log = LoggerFactory.getLogger(RuntimeInfo.class); + + private final Set registeredControllers; + private final Operator operator; + + public RuntimeInfo(Operator operator) { + this.registeredControllers = operator.getRegisteredControllers(); + this.operator = operator; + } + + public boolean isStarted() { + return operator.isStarted(); + } + + public Set getRegisteredControllers() { + checkIfStarted(); + return registeredControllers; + } + + private void checkIfStarted() { + if (!isStarted()) { + log.warn( + "Operator not started yet while accessing runtime info, this might lead to an unreliable behavior"); + } + } + + public boolean allEventSourcesAreHealthy() { + checkIfStarted(); + return registeredControllers.stream() + .filter(rc -> !rc.getControllerHealthInfo().unhealthyEventSources().isEmpty()) + .findFirst().isEmpty(); + } + + /** + * @return Aggregated Map with controller related event sources. + */ + + public Map> unhealthyEventSources() { + checkIfStarted(); + Map> res = new HashMap<>(); + for (var rc : registeredControllers) { + res.put(rc.getConfiguration().getName(), + rc.getControllerHealthInfo().unhealthyEventSources()); + } + return res; + } + + /** + * @return Aggregated Map with controller related event sources that wraps an informer. Thus, + * either a + * {@link io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource} + * or an + * {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}. + */ + public Map> unhealthyInformerWrappingEventSourceHealthIndicator() { + checkIfStarted(); + Map> res = new HashMap<>(); + for (var rc : registeredControllers) { + res.put(rc.getConfiguration().getName(), rc.getControllerHealthInfo() + .unhealthyInformerEventSourceHealthIndicators()); + } + return res; + } +} 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 deleted file mode 100644 index 43c61319ac..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ /dev/null @@ -1,274 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import java.lang.annotation.Annotation; -import java.time.Duration; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; -import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; -import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; -import io.javaoperatorsdk.operator.processing.retry.Retry; - -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET; - -@SuppressWarnings("rawtypes") -public class AnnotationControllerConfiguration

- implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration

{ - - protected final Reconciler

reconciler; - private final ControllerConfiguration annotation; - private List specs; - private Class

resourceClass; - - public AnnotationControllerConfiguration(Reconciler

reconciler) { - this.reconciler = reconciler; - this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); - if (annotation == null) { - throw new OperatorException( - "Missing mandatory @" + ControllerConfiguration.class.getSimpleName() + - " annotation for reconciler: " + reconciler); - } - } - - @Override - public String getName() { - return ReconcilerUtils.getNameFor(reconciler); - } - - @Override - public String getFinalizerName() { - if (annotation == null || annotation.finalizerName().isBlank()) { - return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); - } else { - final var finalizer = annotation.finalizerName(); - if (ReconcilerUtils.isFinalizerValid(finalizer)) { - return finalizer; - } else { - throw new IllegalArgumentException( - finalizer - + " is not a valid finalizer. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers for details"); - } - } - } - - @Override - public boolean isGenerationAware() { - return valueOrDefault( - annotation, ControllerConfiguration::generationAwareEventProcessing, true); - } - - @Override - public Set getNamespaces() { - return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, - DEFAULT_NAMESPACES_SET.toArray(String[]::new))); - } - - @Override - @SuppressWarnings("unchecked") - public Class

getResourceClass() { - if (resourceClass == null) { - resourceClass = - (Class

) Utils.getFirstTypeArgumentFromSuperClassOrInterface(reconciler.getClass(), - Reconciler.class); - } - return resourceClass; - } - - @Override - public String getLabelSelector() { - return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); - } - - @Override - public String getAssociatedReconcilerClassName() { - return reconciler.getClass().getCanonicalName(); - } - - @SuppressWarnings("unchecked") - @Override - public ResourceEventFilter

getEventFilter() { - ResourceEventFilter

answer = null; - - Class>[] filterTypes = - (Class>[]) valueOrDefault(annotation, - ControllerConfiguration::eventFilters, new Object[] {}); - if (filterTypes.length > 0) { - for (var filterType : filterTypes) { - try { - ResourceEventFilter

filter = filterType.getConstructor().newInstance(); - - if (answer == null) { - answer = filter; - } else { - answer = answer.and(filter); - } - } catch (Exception e) { - throw new IllegalArgumentException(e); - } - } - } - return answer != null ? answer : ResourceEventFilters.passthrough(); - } - - @Override - public Optional maxReconciliationInterval() { - final var newConfig = annotation.maxReconciliationInterval(); - if (newConfig != null && newConfig.interval() > 0) { - return Optional.of(Duration.of(newConfig.interval(), newConfig.timeUnit().toChronoUnit())); - } - return Optional.empty(); - } - - @Override - public RateLimiter getRateLimiter() { - final Class rateLimiterClass = annotation.rateLimiter(); - return Utils.instantiateAndConfigureIfNeeded(rateLimiterClass, RateLimiter.class, - Utils.contextFor(this, null, null), this::configureFromAnnotatedReconciler); - } - - @Override - public Retry getRetry() { - final Class retryClass = annotation.retry(); - return Utils.instantiateAndConfigureIfNeeded(retryClass, Retry.class, - Utils.contextFor(this, null, null), this::configureFromAnnotatedReconciler); - } - - - @SuppressWarnings("unchecked") - private void configureFromAnnotatedReconciler(Object instance) { - if (instance instanceof AnnotationConfigurable) { - AnnotationConfigurable configurable = (AnnotationConfigurable) instance; - final Class configurationClass = - (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface( - instance.getClass(), AnnotationConfigurable.class); - final var configAnnotation = reconciler.getClass().getAnnotation(configurationClass); - if (configAnnotation != null) { - configurable.initFrom(configAnnotation); - } - } - } - - @SuppressWarnings("unchecked") - private void configureFromCustomAnnotation(Object instance) { - if (instance instanceof AnnotationDependentResourceConfigurator) { - AnnotationDependentResourceConfigurator configurator = - (AnnotationDependentResourceConfigurator) instance; - final Class configurationClass = - (Class) Utils.getFirstTypeArgumentFromInterface( - instance.getClass(), AnnotationDependentResourceConfigurator.class); - final var configAnnotation = instance.getClass().getAnnotation(configurationClass); - // always called even if the annotation is null so that implementations can provide default - // values - final var config = configurator.configFrom(configAnnotation, this); - configurator.configureWith(config); - } - } - - @Override - @SuppressWarnings("unchecked") - public Optional> onAddFilter() { - return Optional.ofNullable( - Utils.instantiate(annotation.onAddFilter(), OnAddFilter.class, - Utils.contextFor(this, null, null))); - } - - @SuppressWarnings("unchecked") - @Override - public Optional> onUpdateFilter() { - return Optional.ofNullable( - Utils.instantiate(annotation.onUpdateFilter(), OnUpdateFilter.class, - Utils.contextFor(this, null, null))); - } - - @SuppressWarnings("unchecked") - @Override - public Optional> genericFilter() { - return Optional.ofNullable( - Utils.instantiate(annotation.genericFilter(), GenericFilter.class, - Utils.contextFor(this, null, null))); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Override - public List getDependentResources() { - if (specs == null) { - final var dependents = - valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); - if (dependents.length == 0) { - specs = Collections.emptyList(); - return specs; - } - - final var specsMap = new LinkedHashMap(dependents.length); - for (Dependent dependent : dependents) { - final Class dependentType = dependent.type(); - - final var name = getName(dependent, dependentType); - var spec = specsMap.get(name); - if (spec != null) { - throw new IllegalArgumentException( - "A DependentResource named '" + name + "' already exists: " + spec); - } - - final var dependentResource = Utils.instantiateAndConfigureIfNeeded(dependentType, - DependentResource.class, - Utils.contextFor(this, dependentType, Dependent.class), - this::configureFromCustomAnnotation); - - var eventSourceName = dependent.useEventSourceWithName(); - eventSourceName = Constants.NO_VALUE_SET.equals(eventSourceName) ? null : eventSourceName; - final var context = Utils.contextFor(this, dependentType, null); - spec = new DependentResourceSpec(dependentResource, name, - Set.of(dependent.dependsOn()), - Utils.instantiate(dependent.readyPostcondition(), Condition.class, context), - Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context), - Utils.instantiate(dependent.deletePostcondition(), Condition.class, context), - eventSourceName); - specsMap.put(name, spec); - } - - specs = specsMap.values().stream().collect(Collectors.toUnmodifiableList()); - } - return specs; - } - - private String getName(Dependent dependent, Class dependentType) { - var name = dependent.name(); - if (name.isBlank()) { - name = DependentResource.defaultNameFor(dependentType); - } - return name; - } - - public static T valueOrDefault( - ControllerConfiguration controllerConfiguration, - Function mapper, - T defaultValue) { - if (controllerConfiguration == null) { - return defaultValue; - } else { - return mapper.apply(controllerConfiguration); - } - } - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java index 1e4b535202..db91ee22af 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -1,13 +1,40 @@ package io.javaoperatorsdk.operator.api.config; +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.Utils.Configurator; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; +import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; +import io.javaoperatorsdk.operator.processing.retry.Retry; import com.fasterxml.jackson.databind.ObjectMapper; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET; + public class BaseConfigurationService extends AbstractConfigurationService { private static final String LOGGER_NAME = "Default ConfigurationService implementation"; @@ -67,8 +94,140 @@ public ControllerConfiguration getConfigurationFor( return config; } - protected ControllerConfiguration configFor(Reconciler reconciler) { - return new AnnotationControllerConfiguration<>(reconciler); + @SuppressWarnings({"unchecked", "rawtypes"}) + protected

ControllerConfiguration

configFor(Reconciler

reconciler) { + final var annotation = reconciler.getClass().getAnnotation( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.class); + if (annotation == null) { + throw new OperatorException( + "Missing mandatory @" + + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.class + .getSimpleName() + + + " annotation for reconciler: " + reconciler); + } + + final var resourceClass = (Class

) Utils.getFirstTypeArgumentFromSuperClassOrInterface( + reconciler.getClass(), Reconciler.class); + final var name = ReconcilerUtils.getNameFor(reconciler); + final var generationAware = valueOrDefault( + annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::generationAwareEventProcessing, + true); + final var associatedReconcilerClass = + ResolvedControllerConfiguration.getAssociatedReconcilerClassName(reconciler.getClass()); + + final Class retryClass = annotation.retry(); + final var retry = Utils.instantiateAndConfigureIfNeeded(retryClass, Retry.class, + Utils.contextFor(name, null, null), configuratorFor(Retry.class, reconciler)); + + final Class rateLimiterClass = annotation.rateLimiter(); + final var rateLimiter = Utils.instantiateAndConfigureIfNeeded(rateLimiterClass, + RateLimiter.class, + Utils.contextFor(name, null, null), configuratorFor(RateLimiter.class, reconciler)); + + final var reconciliationInterval = annotation.maxReconciliationInterval(); + long interval = -1; + TimeUnit timeUnit = null; + if (reconciliationInterval != null && reconciliationInterval.interval() > 0) { + interval = reconciliationInterval.interval(); + timeUnit = reconciliationInterval.timeUnit(); + } + + final var config = new ResolvedControllerConfiguration

( + resourceClass, name, generationAware, + associatedReconcilerClass, retry, rateLimiter, + ResolvedControllerConfiguration.getMaxReconciliationInterval(interval, timeUnit), + Utils.instantiate(annotation.onAddFilter(), OnAddFilter.class, + Utils.contextFor(name, null, null)), + Utils.instantiate(annotation.onUpdateFilter(), OnUpdateFilter.class, + Utils.contextFor(name, null, null)), + Utils.instantiate(annotation.genericFilter(), GenericFilter.class, + Utils.contextFor(name, null, null)), + Utils.instantiate(annotation.cachePruneFunction(), UnaryOperator.class, + Utils.contextFor(name, null, null)), + Set.of(valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::namespaces, + DEFAULT_NAMESPACES_SET.toArray(String[]::new))), + valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::finalizerName, + Constants.NO_VALUE_SET), + valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::labelSelector, + Constants.NO_VALUE_SET), + null); + + ResourceEventFilter

answer = deprecatedEventFilter(annotation); + config.setEventFilter(answer != null ? answer : ResourceEventFilters.passthrough()); + + List specs = dependentResources(annotation, config); + config.setDependentResources(specs); + + return config; + } + + @SuppressWarnings("unchecked") + private static

ResourceEventFilter

deprecatedEventFilter( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration annotation) { + ResourceEventFilter

answer = null; + + Class>[] filterTypes = + (Class>[]) valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::eventFilters, + new Object[] {}); + for (var filterType : filterTypes) { + try { + ResourceEventFilter

filter = filterType.getConstructor().newInstance(); + + if (answer == null) { + answer = filter; + } else { + answer = answer.and(filter); + } + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + return answer; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static List dependentResources( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration annotation, + ControllerConfiguration parent) { + final var dependents = + valueOrDefault(annotation, + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration::dependents, + new Dependent[] {}); + if (dependents.length == 0) { + return Collections.emptyList(); + } + + final var specsMap = new LinkedHashMap(dependents.length); + for (Dependent dependent : dependents) { + final Class dependentType = dependent.type(); + + final var dependentName = getName(dependent.name(), dependentType); + var spec = specsMap.get(dependentName); + if (spec != null) { + throw new IllegalArgumentException( + "A DependentResource named '" + dependentName + "' already exists: " + spec); + } + + final var name = parent.getName(); + + var eventSourceName = dependent.useEventSourceWithName(); + eventSourceName = Constants.NO_VALUE_SET.equals(eventSourceName) ? null : eventSourceName; + final var context = Utils.contextFor(name, dependentType, null); + spec = new DependentResourceSpec(dependentType, dependentName, + Set.of(dependent.dependsOn()), + Utils.instantiate(dependent.readyPostcondition(), Condition.class, context), + Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context), + Utils.instantiate(dependent.deletePostcondition(), Condition.class, context), + eventSourceName); + specsMap.put(dependentName, spec); + } + return specsMap.values().stream().collect(Collectors.toUnmodifiableList()); } protected boolean createIfNeeded() { @@ -79,4 +238,43 @@ protected boolean createIfNeeded() { public boolean checkCRDAndValidateLocalModel() { return Utils.shouldCheckCRDAndValidateLocalModel(); } + + private static T valueOrDefault( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration controllerConfiguration, + Function mapper, + T defaultValue) { + if (controllerConfiguration == null) { + return defaultValue; + } else { + return mapper.apply(controllerConfiguration); + } + } + + @SuppressWarnings("rawtypes") + private static String getName(String name, Class dependentType) { + if (name.isBlank()) { + name = DependentResource.defaultNameFor(dependentType); + } + return name; + } + + @SuppressWarnings("unused") + private static Configurator configuratorFor(Class instanceType, + Reconciler reconciler) { + return instance -> configureFromAnnotatedReconciler(instance, reconciler); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void configureFromAnnotatedReconciler(Object instance, Reconciler reconciler) { + if (instance instanceof AnnotationConfigurable) { + AnnotationConfigurable configurable = (AnnotationConfigurable) instance; + final Class configurationClass = + (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface( + instance.getClass(), AnnotationConfigurable.class); + final var configAnnotation = reconciler.getClass().getAnnotation(configurationClass); + if (configAnnotation != null) { + configurable.initFrom(configAnnotation); + } + } + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java index 036aa76bbf..45d7162184 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java @@ -145,9 +145,9 @@ default ObjectMapper getObjectMapper() { return Serialization.jsonMapper(); } - @Deprecated(forRemoval = true) + @SuppressWarnings("rawtypes") default DependentResourceFactory dependentResourceFactory() { - return null; + return DependentResourceFactory.DEFAULT; } default Optional getLeaderElectionConfiguration() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java index 74c40ed9a8..b2640777a7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java @@ -17,19 +17,37 @@ import io.javaoperatorsdk.operator.processing.retry.GradualRetry; import io.javaoperatorsdk.operator.processing.retry.Retry; -public interface ControllerConfiguration extends ResourceConfiguration { +public interface ControllerConfiguration

extends ResourceConfiguration

{ @SuppressWarnings("rawtypes") RateLimiter DEFAULT_RATE_LIMITER = LinearRateLimiter.deactivatedRateLimiter(); default String getName() { - return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); + return ensureValidName(null, getAssociatedReconcilerClassName()); } default String getFinalizerName() { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } + static String ensureValidName(String name, String reconcilerClassName) { + return name != null ? name : ReconcilerUtils.getDefaultReconcilerName(reconcilerClassName); + } + + static String ensureValidFinalizerName(String finalizer, String resourceTypeName) { + if (finalizer != null && !finalizer.isBlank()) { + if (ReconcilerUtils.isFinalizerValid(finalizer)) { + return finalizer; + } else { + throw new IllegalArgumentException( + finalizer + + " is not a valid finalizer. See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers for details"); + } + } else { + return ReconcilerUtils.getDefaultFinalizerName(resourceTypeName); + } + } + default boolean isGenerationAware() { return true; } @@ -70,8 +88,12 @@ default RateLimiter getRateLimiter() { *

* * @return filter + * @deprecated use {@link ResourceConfiguration#onAddFilter()}, + * {@link ResourceConfiguration#onUpdateFilter()} or + * {@link ResourceConfiguration#genericFilter()} instead */ - default ResourceEventFilter getEventFilter() { + @Deprecated(forRemoval = true) + default ResourceEventFilter

getEventFilter() { return ResourceEventFilters.passthrough(); } @@ -91,8 +113,8 @@ default ConfigurationService getConfigurationService() { @SuppressWarnings("unchecked") @Override - default Class getResourceClass() { - return (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), + default Class

getResourceClass() { + return (Class

) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), ControllerConfiguration.class); } } 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 c36aa51d2e..789de127f5 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 @@ -1,16 +1,15 @@ package io.javaoperatorsdk.operator.api.config; import java.time.Duration; +import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Optional; +import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; +import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; @@ -33,11 +32,12 @@ public class ControllerConfigurationOverrider { private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private Duration reconciliationMaxInterval; - private final LinkedHashMap namedDependentResourceSpecs; private OnAddFilter onAddFilter; private OnUpdateFilter onUpdateFilter; private GenericFilter genericFilter; private RateLimiter rateLimiter; + private UnaryOperator cachePruneFunction; + private Map configurations; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizerName(); @@ -47,15 +47,12 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { labelSelector = original.getLabelSelector(); customResourcePredicate = original.getEventFilter(); reconciliationMaxInterval = original.maxReconciliationInterval().orElse(null); - // make the original specs modifiable - final var dependentResources = original.getDependentResources(); - namedDependentResourceSpecs = new LinkedHashMap<>(dependentResources.size()); this.onAddFilter = original.onAddFilter().orElse(null); this.onUpdateFilter = original.onUpdateFilter().orElse(null); this.genericFilter = original.genericFilter().orElse(null); - dependentResources.forEach(drs -> namedDependentResourceSpecs.put(drs.getName(), drs)); this.original = original; this.rateLimiter = original.getRateLimiter(); + this.cachePruneFunction = original.cachePruneFunction().orElse(null); } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -158,57 +155,38 @@ public ControllerConfigurationOverrider withGenericFilter(GenericFilter ge return this; } - @SuppressWarnings("unchecked") + public ControllerConfigurationOverrider withCachePruneFunction( + UnaryOperator cachePruneFunction) { + this.cachePruneFunction = cachePruneFunction; + return this; + } + public ControllerConfigurationOverrider replacingNamedDependentResourceConfig(String name, Object dependentResourceConfig) { - var current = namedDependentResourceSpecs.get(name); - if (current == null) { - throw new IllegalArgumentException("Cannot find a DependentResource named: " + name); - } + final var specs = original.getDependentResources(); + final var spec = specs.stream() + .filter(drs -> drs.getName().equals(name)).findFirst() + .orElseThrow( + () -> new IllegalArgumentException("Cannot find a DependentResource named: " + name)); - var dependentResource = current.getDependentResource(); - if (dependentResource instanceof DependentResourceConfigurator) { - var configurator = (DependentResourceConfigurator) dependentResource; - configurator.configureWith(dependentResourceConfig); + if (configurations == null) { + configurations = new HashMap<>(specs.size()); } + configurations.put(spec, dependentResourceConfig); return this; } public ControllerConfiguration build() { - final var hasModifiedNamespaces = !original.getNamespaces().equals(namespaces); - final var newDependentSpecs = namedDependentResourceSpecs.values().stream() - .peek(spec -> { - // if the dependent resource has a NamespaceChangeable config - // update the namespaces if needed, otherwise, do nothing - if (hasModifiedNamespaces) { - final Optional maybeConfig = spec.getDependentResourceConfiguration(); - maybeConfig - .filter(NamespaceChangeable.class::isInstance) - .map(NamespaceChangeable.class::cast) - .filter(NamespaceChangeable::allowsNamespaceChanges) - .ifPresent(nc -> nc.changeNamespaces(namespaces)); - } - }).collect(Collectors.toList()); - - return new DefaultControllerConfiguration<>( - original.getAssociatedReconcilerClassName(), - original.getName(), - original.getResourceTypeName(), - finalizer, - generationAware, - namespaces, - retry, - labelSelector, - customResourcePredicate, - original.getResourceClass(), - reconciliationMaxInterval, - onAddFilter, - onUpdateFilter, - genericFilter, - rateLimiter, - newDependentSpecs); + final var overridden = new ResolvedControllerConfiguration<>( + original.getResourceClass(), original.getName(), + generationAware, original.getAssociatedReconcilerClassName(), retry, rateLimiter, + reconciliationMaxInterval, onAddFilter, onUpdateFilter, genericFilter, cachePruneFunction, + original.getDependentResources(), + namespaces, finalizer, labelSelector, configurations); + overridden.setEventFilter(customResourcePredicate); + return overridden; } public static ControllerConfigurationOverrider override( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java deleted file mode 100644 index 3f4d952133..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java +++ /dev/null @@ -1,119 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; -import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; -import io.javaoperatorsdk.operator.processing.retry.Retry; - -@SuppressWarnings("rawtypes") -public class DefaultControllerConfiguration - extends DefaultResourceConfiguration - implements ControllerConfiguration { - - private final String associatedControllerClassName; - private final String name; - private final String crdName; - private final String finalizer; - private final boolean generationAware; - private final Retry retry; - private final ResourceEventFilter resourceEventFilter; - private final List dependents; - private final Duration reconciliationMaxInterval; - private final RateLimiter rateLimiter; - - // NOSONAR constructor is meant to provide all information - public DefaultControllerConfiguration( - String associatedControllerClassName, - String name, - String crdName, - String finalizer, - boolean generationAware, - Set namespaces, - Retry retry, - String labelSelector, - ResourceEventFilter resourceEventFilter, - Class resourceClass, - Duration reconciliationMaxInterval, - OnAddFilter onAddFilter, - OnUpdateFilter onUpdateFilter, - GenericFilter genericFilter, - RateLimiter rateLimiter, - List dependents) { - super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces); - this.associatedControllerClassName = associatedControllerClassName; - this.name = name; - this.crdName = crdName; - this.finalizer = finalizer; - this.generationAware = generationAware; - this.reconciliationMaxInterval = reconciliationMaxInterval; - this.retry = - retry == null - ? ControllerConfiguration.super.getRetry() - : retry; - this.resourceEventFilter = resourceEventFilter; - this.rateLimiter = - rateLimiter != null ? rateLimiter : LinearRateLimiter.deactivatedRateLimiter(); - this.dependents = dependents != null ? dependents : Collections.emptyList(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getResourceTypeName() { - return crdName; - } - - @Override - public String getFinalizerName() { - return finalizer; - } - - @Override - public boolean isGenerationAware() { - return generationAware; - } - - @Override - public String getAssociatedReconcilerClassName() { - return associatedControllerClassName; - } - - @Override - public Retry getRetry() { - return retry; - } - - @Override - public ResourceEventFilter getEventFilter() { - return resourceEventFilter; - } - - @Override - public List getDependentResources() { - return dependents; - } - - @Override - public Optional maxReconciliationInterval() { - return Optional.ofNullable(reconciliationMaxInterval); - } - - @Override - public RateLimiter getRateLimiter() { - return rateLimiter; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java index 9bc6ce5dba..bb9f365d58 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java @@ -2,48 +2,44 @@ import java.util.Optional; import java.util.Set; +import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET; - public class DefaultResourceConfiguration implements ResourceConfiguration { - private final String labelSelector; - private final Set namespaces; private final Class resourceClass; + private final String resourceTypeName; private final OnAddFilter onAddFilter; private final OnUpdateFilter onUpdateFilter; private final GenericFilter genericFilter; + private final String labelSelector; + private final Set namespaces; + private final UnaryOperator cachePruneFunction; - public DefaultResourceConfiguration(String labelSelector, Class resourceClass, - OnAddFilter onAddFilter, - OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, String... namespaces) { - this(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, - namespaces == null || namespaces.length == 0 ? DEFAULT_NAMESPACES_SET - : Set.of(namespaces)); - } - - public DefaultResourceConfiguration(String labelSelector, Class resourceClass, - OnAddFilter onAddFilter, - OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, Set namespaces) { - this.labelSelector = labelSelector; + protected DefaultResourceConfiguration(Class resourceClass, + Set namespaces, String labelSelector, OnAddFilter onAddFilter, + OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, + UnaryOperator cachePruneFunction) { this.resourceClass = resourceClass; + this.resourceTypeName = ReconcilerUtils.getResourceTypeName(resourceClass); this.onAddFilter = onAddFilter; this.onUpdateFilter = onUpdateFilter; this.genericFilter = genericFilter; - this.namespaces = - namespaces == null || namespaces.isEmpty() ? DEFAULT_NAMESPACES_SET - : namespaces; + + this.namespaces = ResourceConfiguration.ensureValidNamespaces(namespaces); + this.labelSelector = ResourceConfiguration.ensureValidLabelSelector(labelSelector); + this.cachePruneFunction = cachePruneFunction; } @Override public String getResourceTypeName() { - return ResourceConfiguration.super.getResourceTypeName(); + return resourceTypeName; } @Override @@ -56,6 +52,11 @@ public Set getNamespaces() { return namespaces; } + @Override + public Optional> cachePruneFunction() { + return Optional.ofNullable(this.cachePruneFunction); + } + @Override public Class getResourceClass() { return resourceClass; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java new file mode 100644 index 0000000000..b1f653cb75 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResolvedControllerConfiguration.java @@ -0,0 +1,169 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.UnaryOperator; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationProvider; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; +import io.javaoperatorsdk.operator.processing.retry.Retry; + +@SuppressWarnings("rawtypes") +public class ResolvedControllerConfiguration

+ extends DefaultResourceConfiguration

+ implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration

, + DependentResourceConfigurationProvider { + + private final String name; + private final boolean generationAware; + private final String associatedReconcilerClassName; + private final Retry retry; + private final RateLimiter rateLimiter; + private final Optional maxReconciliationInterval; + private final String finalizer; + private final Map configurations; + + private ResourceEventFilter

eventFilter; + private List dependentResources; + + public ResolvedControllerConfiguration(Class

resourceClass, ControllerConfiguration

other) { + this(resourceClass, other.getName(), other.isGenerationAware(), + other.getAssociatedReconcilerClassName(), other.getRetry(), other.getRateLimiter(), + other.maxReconciliationInterval().orElse(null), + other.onAddFilter().orElse(null), other.onUpdateFilter().orElse(null), + other.genericFilter().orElse(null), other.cachePruneFunction().orElse(null), + other.getDependentResources(), other.getNamespaces(), + other.getFinalizerName(), other.getLabelSelector(), Collections.emptyMap()); + } + + public static Duration getMaxReconciliationInterval(long interval, TimeUnit timeUnit) { + return interval > 0 ? Duration.of(interval, timeUnit.toChronoUnit()) : null; + } + + public static String getAssociatedReconcilerClassName( + Class reconcilerClass) { + return reconcilerClass.getCanonicalName(); + } + + protected Retry ensureRetry(Retry given) { + return given == null ? ControllerConfiguration.super.getRetry() : given; + } + + protected RateLimiter ensureRateLimiter(RateLimiter given) { + return given == null ? ControllerConfiguration.super.getRateLimiter() : given; + } + + public ResolvedControllerConfiguration(Class

resourceClass, String name, + boolean generationAware, String associatedReconcilerClassName, Retry retry, + RateLimiter rateLimiter, Duration maxReconciliationInterval, + OnAddFilter

onAddFilter, OnUpdateFilter

onUpdateFilter, + GenericFilter

genericFilter, UnaryOperator

cachePruneFunction, + List dependentResources, + Set namespaces, String finalizer, String labelSelector, + Map configurations) { + this(resourceClass, name, generationAware, associatedReconcilerClassName, retry, rateLimiter, + maxReconciliationInterval, onAddFilter, onUpdateFilter, genericFilter, cachePruneFunction, + namespaces, finalizer, labelSelector, configurations); + setDependentResources(dependentResources); + } + + protected ResolvedControllerConfiguration(Class

resourceClass, String name, + boolean generationAware, String associatedReconcilerClassName, Retry retry, + RateLimiter rateLimiter, Duration maxReconciliationInterval, + OnAddFilter

onAddFilter, OnUpdateFilter

onUpdateFilter, GenericFilter

genericFilter, + UnaryOperator

cachePruneFunction, + Set namespaces, String finalizer, String labelSelector, + Map configurations) { + super(resourceClass, namespaces, labelSelector, onAddFilter, onUpdateFilter, genericFilter, + cachePruneFunction); + this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName); + this.generationAware = generationAware; + this.associatedReconcilerClassName = associatedReconcilerClassName; + this.retry = ensureRetry(retry); + this.rateLimiter = ensureRateLimiter(rateLimiter); + this.maxReconciliationInterval = Optional.ofNullable(maxReconciliationInterval); + this.configurations = configurations != null ? configurations : Collections.emptyMap(); + + this.finalizer = + ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName()); + } + + protected ResolvedControllerConfiguration(Class

resourceClass, String name, + Class reconcilerClas) { + this(resourceClass, name, false, getAssociatedReconcilerClassName(reconcilerClas), null, null, + null, null, null, null, null, + null, null, null, null); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getFinalizerName() { + return finalizer; + } + + @Override + public boolean isGenerationAware() { + return generationAware; + } + + @Override + public String getAssociatedReconcilerClassName() { + return associatedReconcilerClassName; + } + + @Override + public Retry getRetry() { + return retry; + } + + @Override + public RateLimiter getRateLimiter() { + return rateLimiter; + } + + @Override + public List getDependentResources() { + return dependentResources; + } + + protected void setDependentResources(List dependentResources) { + this.dependentResources = dependentResources == null ? Collections.emptyList() + : Collections.unmodifiableList(dependentResources); + } + + @Override + public Optional maxReconciliationInterval() { + return maxReconciliationInterval; + } + + @Override + public ResourceEventFilter

getEventFilter() { + return eventFilter; + } + + @Deprecated(forRemoval = true) + protected void setEventFilter(ResourceEventFilter

eventFilter) { + this.eventFilter = eventFilter; + } + + @Override + public Object getConfigurationFor(DependentResourceSpec spec) { + return configurations.get(spec); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java index 90e18f3e52..85f3bc3b36 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java @@ -1,13 +1,16 @@ package io.javaoperatorsdk.operator.api.config; +import java.util.Collection; import java.util.Collections; import java.util.Optional; import java.util.Set; +import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; @@ -45,6 +48,11 @@ default String getLabelSelector() { return null; } + static String ensureValidLabelSelector(String labelSelector) { + // might want to implement validation here? + return labelSelector; + } + @SuppressWarnings("unchecked") default Class getResourceClass() { return (Class) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(), @@ -88,6 +96,14 @@ static void failIfNotValid(Set namespaces) { + Constants.WATCH_CURRENT_NAMESPACE + "'"); } + static Set ensureValidNamespaces(Collection namespaces) { + if (namespaces != null && !namespaces.isEmpty()) { + return Set.copyOf(namespaces); + } else { + return Constants.DEFAULT_NAMESPACES_SET; + } + } + /** * Computes the effective namespaces based on the set specified by the user, in particular * retrieves the current namespace from the client when the user specified that they wanted to @@ -108,4 +124,11 @@ default Set getEffectiveNamespaces() { } return targetNamespaces; } + + /** + * See {@link ControllerConfiguration#cachePruneFunction()} for details. + */ + default Optional> cachePruneFunction() { + return Optional.empty(); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java index fd0f612fa1..5838eb9b97 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java @@ -227,6 +227,13 @@ public interface Configurator { public static String contextFor(ControllerConfiguration controllerConfiguration, Class dependentType, Class configurationAnnotation) { + return contextFor(controllerConfiguration.getName(), dependentType, configurationAnnotation); + } + + @SuppressWarnings("rawtypes") + public static String contextFor(String reconcilerName, + Class dependentType, + Class configurationAnnotation) { final var annotationName = configurationAnnotation != null ? configurationAnnotation.getSimpleName() : io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.class @@ -235,8 +242,7 @@ public static String contextFor(ControllerConfiguration controllerConfigurati if (dependentType != null) { context += "DependentResource: " + dependentType.getName() + ", "; } - context += "reconciler: " + controllerConfiguration.getName(); - + context += "reconciler: " + reconcilerName; return context; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/ConfigurationConverter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/ConfigurationConverter.java new file mode 100644 index 0000000000..68e0f521de --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/ConfigurationConverter.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.lang.annotation.Annotation; + +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; + +public interface ConfigurationConverter> { + + C configFrom(A configAnnotation, ControllerConfiguration parentConfiguration, + Class originatingClass); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Configured.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Configured.java new file mode 100644 index 0000000000..db8c6f6db3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Configured.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Configured { + + Class by(); + + Class with(); + + @SuppressWarnings("rawtypes") + Class converter(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java new file mode 100644 index 0000000000..a0c9dc67ae --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationProvider.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +public interface DependentResourceConfigurationProvider { + @SuppressWarnings("rawtypes") + Object getConfigurationFor(DependentResourceSpec spec); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolver.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolver.java new file mode 100644 index 0000000000..9a143fc57c --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolver.java @@ -0,0 +1,181 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class DependentResourceConfigurationResolver { + + private DependentResourceConfigurationResolver() {} + + private static final Map, ConverterAnnotationPair> converters = + new HashMap<>(); + private static final Map, ConfigurationConverter> knownConverters = + new HashMap<>(); + + public static > void configure( + DependentResource dependentResource, DependentResourceSpec spec, C parentConfiguration) { + if (dependentResource instanceof DependentResourceConfigurator) { + final var configurator = (DependentResourceConfigurator) dependentResource; + final var config = configurationFor(spec, parentConfiguration); + configurator.configureWith(config); + } + } + + public static > Object configurationFor( + DependentResourceSpec spec, C parentConfiguration) { + + // first check if the parent configuration has potentially already resolved the configuration + if (parentConfiguration instanceof DependentResourceConfigurationProvider) { + final var provider = (DependentResourceConfigurationProvider) parentConfiguration; + final var configuration = provider.getConfigurationFor(spec); + if (configuration != null) { + return configuration; + } + } + + // find Configured-annotated class if it exists + return extractConfigurationFromConfigured(spec.getDependentResourceClass(), + parentConfiguration); + } + + public static > Object extractConfigurationFromConfigured( + Class dependentResourceClass, C parentConfiguration) { + var converterAnnotationPair = converters.get(dependentResourceClass); + + Annotation configAnnotation; + if (converterAnnotationPair == null) { + var configuredClassPair = getConfigured(dependentResourceClass); + if (configuredClassPair == null) { + return null; + } + + // check if we already have a converter registered for the found Configured annotated class + converterAnnotationPair = converters.get(configuredClassPair.annotatedClass); + if (converterAnnotationPair == null) { + final var configured = configuredClassPair.configured; + converterAnnotationPair = + getOrCreateConverter(dependentResourceClass, parentConfiguration, + configured.converter(), + configured.by()); + } else { + // only register the converter pair for this dependent resource class as well + converters.put(dependentResourceClass, converterAnnotationPair); + } + } + + // find the associated configuration annotation + configAnnotation = + dependentResourceClass.getAnnotation(converterAnnotationPair.annotationClass); + final var converter = converterAnnotationPair.converter; + + // always called even if the annotation is null so that implementations can provide default + // values + return converter.configFrom(configAnnotation, parentConfiguration, dependentResourceClass); + } + + private static ConfiguredClassPair getConfigured( + Class dependentResourceClass) { + Class currentClass = dependentResourceClass; + Configured configured; + ConfiguredClassPair result = null; + while (DependentResource.class.isAssignableFrom(currentClass)) { + configured = currentClass.getAnnotation(Configured.class); + if (configured != null) { + result = new ConfiguredClassPair(configured, currentClass); + break; + } + currentClass = (Class) currentClass.getSuperclass(); + } + return result; + } + + private static > ConverterAnnotationPair getOrCreateConverter( + Class dependentResourceClass, C parentConfiguration, + Class converterClass, + Class annotationClass) { + var converterPair = converters.get(dependentResourceClass); + if (converterPair == null) { + // only instantiate a new converter if we haven't done so already for this converter type + var converter = knownConverters.get(converterClass); + if (converter == null) { + converter = Utils.instantiate(converterClass, + ConfigurationConverter.class, + Utils.contextFor(parentConfiguration, dependentResourceClass, Configured.class)); + knownConverters.put(converterClass, converter); + } + // record dependent class - converter association for faster future retrieval + converterPair = new ConverterAnnotationPair(converter, annotationClass); + converters.put(dependentResourceClass, converterPair); + } + return converterPair; + } + + static ConfigurationConverter getConverter( + Class dependentResourceClass) { + final var converterAnnotationPair = converters.get(dependentResourceClass); + return converterAnnotationPair != null ? converterAnnotationPair.converter : null; + } + + @SuppressWarnings("unused") + public static void registerConverter(Class dependentResourceClass, + ConfigurationConverter converter) { + var configured = getConfigured(dependentResourceClass); + if (configured == null) { + throw new IllegalArgumentException("There is no @" + Configured.class.getSimpleName() + + " annotation on " + dependentResourceClass.getName() + + " or its superclasses and thus doesn't need to be associated with a converter"); + } + + // find the associated configuration annotation + final var toRegister = new ConverterAnnotationPair(converter, configured.configured.by()); + final Class converterClass = converter.getClass(); + converters.put(dependentResourceClass, toRegister); + + // also register the Configured-annotated class if not the one we're registering + if (!dependentResourceClass.equals(configured.annotatedClass)) { + converters.put(configured.annotatedClass, toRegister); + } + + knownConverters.put(converterClass, converter); + } + + private static class ConfiguredClassPair { + private final Configured configured; + private final Class annotatedClass; + + private ConfiguredClassPair(Configured configured, + Class annotatedClass) { + this.configured = configured; + this.annotatedClass = annotatedClass; + } + + @Override + public String toString() { + return annotatedClass.getName() + " -> " + configured; + } + } + + private static class ConverterAnnotationPair { + private final ConfigurationConverter converter; + private final Class annotationClass; + + private ConverterAnnotationPair(ConfigurationConverter converter, + Class annotationClass) { + this.converter = converter; + this.annotationClass = annotationClass; + } + + @Override + public String toString() { + return converter.toString() + " -> " + annotationClass.getName(); + } + } +} 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 5d0b8c6b01..58fd9ace4b 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 @@ -6,12 +6,11 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; -public class DependentResourceSpec { +public class DependentResourceSpec { - private final DependentResource dependentResource; + private final Class> dependentResourceClass; private final String name; @@ -25,11 +24,11 @@ public class DependentResourceSpec { private final String useEventSourceWithName; - public DependentResourceSpec(DependentResource dependentResource, + public DependentResourceSpec(Class> dependentResourceClass, String name, Set dependsOn, Condition readyCondition, Condition reconcileCondition, Condition deletePostCondition, String useEventSourceWithName) { - this.dependentResource = dependentResource; + this.dependentResourceClass = dependentResourceClass; this.name = name; this.dependsOn = dependsOn; this.readyCondition = readyCondition; @@ -38,28 +37,8 @@ public DependentResourceSpec(DependentResource dependentResource, this.useEventSourceWithName = useEventSourceWithName; } - public DependentResourceSpec(DependentResourceSpec other) { - this.dependentResource = other.dependentResource; - this.name = other.name; - this.dependsOn = other.dependsOn; - this.readyCondition = other.readyCondition; - this.reconcileCondition = other.reconcileCondition; - this.deletePostCondition = other.deletePostCondition; - this.useEventSourceWithName = other.useEventSourceWithName; - } - - @SuppressWarnings("unchecked") - public Class> getDependentResourceClass() { - return (Class>) dependentResource.getClass(); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - public Optional getDependentResourceConfiguration() { - if (dependentResource instanceof DependentResourceConfigurator) { - var configurator = (DependentResourceConfigurator) dependentResource; - return configurator.configuration(); - } - return Optional.empty(); + public Class> getDependentResourceClass() { + return dependentResourceClass; } public String getName() { @@ -80,7 +59,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - DependentResourceSpec that = (DependentResourceSpec) o; + DependentResourceSpec that = (DependentResourceSpec) o; return name.equals(that.name); } @@ -108,10 +87,6 @@ public Condition getDeletePostCondition() { return deletePostCondition; } - public DependentResource getDependentResource() { - return dependentResource; - } - public Optional getUseEventSourceWithName() { return Optional.ofNullable(useEventSourceWithName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 52f71501a9..e26597dde5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -3,6 +3,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.UnaryOperator; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; @@ -38,8 +39,10 @@ protected DefaultInformerConfiguration(String labelSelector, OnAddFilter onAddFilter, OnUpdateFilter onUpdateFilter, OnDeleteFilter onDeleteFilter, - GenericFilter genericFilter) { - super(labelSelector, resourceClass, onAddFilter, onUpdateFilter, genericFilter, namespaces); + GenericFilter genericFilter, + UnaryOperator cachePruneFunction) { + super(resourceClass, namespaces, labelSelector, onAddFilter, onUpdateFilter, genericFilter, + cachePruneFunction); this.followControllerNamespaceChanges = followControllerNamespaceChanges; this.primaryToSecondaryMapper = primaryToSecondaryMapper; @@ -64,6 +67,7 @@ public Optional> onDeleteFilter() { } @Override + @SuppressWarnings("unchecked") public

PrimaryToSecondaryMapper

getPrimaryToSecondaryMapper() { return (PrimaryToSecondaryMapper

) primaryToSecondaryMapper; } @@ -102,6 +106,7 @@ class InformerConfigurationBuilder { private OnDeleteFilter onDeleteFilter; private GenericFilter genericFilter; private boolean inheritControllerNamespacesOnChange = false; + private UnaryOperator cachePruneFunction; private InformerConfigurationBuilder(Class resourceClass) { this.resourceClass = resourceClass; @@ -202,12 +207,18 @@ public InformerConfigurationBuilder withGenericFilter(GenericFilter generi return this; } + public InformerConfigurationBuilder withCachePruneFunction( + UnaryOperator cachePruneFunction) { + this.cachePruneFunction = cachePruneFunction; + return this; + } + public InformerConfiguration build() { return new DefaultInformerConfiguration<>(labelSelector, resourceClass, primaryToSecondaryMapper, secondaryToPrimaryMapper, namespaces, inheritControllerNamespacesOnChange, onAddFilter, onUpdateFilter, - onDeleteFilter, genericFilter); + onDeleteFilter, genericFilter, cachePruneFunction); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java index be608b098e..c134a5522f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -20,6 +21,11 @@ public interface Metrics { */ Metrics NOOP = new Metrics() {}; + /** + * Do initialization if necessary; + */ + default void controllerRegistered(Controller controller) {} + /** * Called when an event has been accepted by the SDK from an event source, which would result in * potentially triggering the associated Reconciler. @@ -29,55 +35,44 @@ public interface Metrics { */ default void receivedEvent(Event event, Map metadata) {} - /** - * - * @deprecated Use (and implement) {@link #receivedEvent(Event, Map)} instead - */ - @Deprecated - default void receivedEvent(Event event) { - receivedEvent(event, Collections.emptyMap()); - } - - /** - * - * @deprecated Use (and implement) {@link #reconcileCustomResource(ResourceID, RetryInfo, Map)} - * instead - */ - @Deprecated - default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) { - reconcileCustomResource(resourceID, retryInfo, Collections.emptyMap()); - } + @Deprecated(forRemoval = true) + default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo, + Map metadata) {} /** * Called right before a resource is dispatched to the ExecutorService for reconciliation. * - * @param resourceID the {@link ResourceID} associated with the resource + * @param resource the associated with the resource * @param retryInfo the current retry state information for the reconciliation request * @param metadata metadata associated with the resource being processed */ - default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo, - Map metadata) {} - - /** - * - * @deprecated Use (and implement) {@link #failedReconciliation(ResourceID, Exception, Map)} - * instead - */ - @Deprecated - default void failedReconciliation(ResourceID resourceID, Exception exception) { - failedReconciliation(resourceID, exception, Collections.emptyMap()); + default void reconcileCustomResource(HasMetadata resource, RetryInfo retryInfo, + Map metadata) { + reconcileCustomResource(ResourceID.fromResource(resource), retryInfo, metadata); } + @Deprecated(forRemoval = true) + default void failedReconciliation(ResourceID resourceID, Exception exception, + Map metadata) {} + /** * Called when a precedent reconciliation for the resource associated with the specified * {@link ResourceID} resulted in the provided exception, resulting in a retry of the * reconciliation. * - * @param resourceID the {@link ResourceID} associated with the resource being processed + * @param resource the {@link ResourceID} associated with the resource being processed * @param exception the exception that caused the failed reconciliation resulting in a retry * @param metadata metadata associated with the resource being processed */ - default void failedReconciliation(ResourceID resourceID, Exception exception, + default void failedReconciliation(HasMetadata resource, Exception exception, + Map metadata) { + failedReconciliation(ResourceID.fromResource(resource), exception, metadata); + } + + + default void reconciliationExecutionStarted(HasMetadata resource, Map metadata) {} + + default void reconciliationExecutionFinished(HasMetadata resource, Map metadata) {} /** @@ -107,16 +102,21 @@ default void finishedReconciliation(ResourceID resourceID) { finishedReconciliation(resourceID, Collections.emptyMap()); } + @Deprecated(forRemoval = true) + default void finishedReconciliation(ResourceID resourceID, Map metadata) {} + /** * Called when the * {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler#reconcile(HasMetadata, Context)} * method of the Reconciler associated with the resource associated with the specified * {@link ResourceID} has sucessfully finished. * - * @param resourceID the {@link ResourceID} associated with the resource being processed + * @param resource the {@link ResourceID} associated with the resource being processed * @param metadata metadata associated with the resource being processed */ - default void finishedReconciliation(ResourceID resourceID, Map metadata) {} + default void finishedReconciliation(HasMetadata resource, Map metadata) { + finishedReconciliation(ResourceID.fromResource(resource), metadata); + } /** * Encapsulates the information about a controller execution i.e. a call to either diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java index 55a9cbdbed..08aeb16f90 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -21,6 +21,7 @@ public final class Constants { public static final String SAME_AS_CONTROLLER = "JOSDK_SAME_AS_CONTROLLER"; public static final String RESOURCE_GVK_KEY = "josdk.resource.gvk"; + public static final String CONTROLLER_NAME = "controller.name"; private Constants() {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index ec76adf89d..df3efa7d40 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -5,6 +5,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.function.UnaryOperator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; @@ -118,4 +119,25 @@ MaxReconciliationInterval maxReconciliationInterval() default @MaxReconciliation * accessible no-arg constructor. */ Class rateLimiter() default LinearRateLimiter.class; + + /** + *

+ * This is an experimental feature, might be a subject of change and even removal in the + * future. + *

+ *

+ * In order to optimize cache, thus set null on some attributes, this function can be set. Note + * that this has subtle implications how updates on the resources should be handled. Notably only + * patching of the resource can be used from that point, since update would remove not cached + * parts of the resource. + *

+ *

+ * Note that this feature does not work with Dependent Resources. + *

+ * + * + * + * @return function to remove parts of the resource. + */ + Class cachePruneFunction() default UnaryOperator.class; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java index 139d70b002..e9e47f6d97 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java @@ -1,18 +1,21 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -@Deprecated +import static io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver.configure; + @SuppressWarnings({"rawtypes", "unchecked"}) -public interface DependentResourceFactory { +public interface DependentResourceFactory> { - default DependentResource createFrom(DependentResourceSpec spec) { - return createFrom(spec.getDependentResourceClass()); - } + DependentResourceFactory DEFAULT = new DependentResourceFactory() {}; - default T createFrom(Class dependentResourceClass) { - return (T) Utils.instantiate(dependentResourceClass, DependentResource.class, null); + default DependentResource createFrom(DependentResourceSpec spec, C configuration) { + final var dependentResourceClass = spec.getDependentResourceClass(); + return Utils.instantiateAndConfigureIfNeeded(dependentResourceClass, + DependentResource.class, + Utils.contextFor(configuration, dependentResourceClass, Dependent.class), + (instance) -> configure(instance, spec, configuration)); } - } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java deleted file mode 100644 index d65249b753..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/AnnotationDependentResourceConfigurator.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; - -import java.lang.annotation.Annotation; - -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; - -public interface AnnotationDependentResourceConfigurator
- extends DependentResourceConfigurator { - - C configFrom(A annotation, ControllerConfiguration parentConfiguration); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java new file mode 100644 index 0000000000..2adb3a8508 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.health; + +import java.util.Map; +import java.util.stream.Collectors; + +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; + +@SuppressWarnings("rawtypes") +public class ControllerHealthInfo { + + private EventSourceManager eventSourceManager; + + public ControllerHealthInfo(EventSourceManager eventSourceManager) { + this.eventSourceManager = eventSourceManager; + } + + public Map eventSourceHealthIndicators() { + return eventSourceManager.allEventSources().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public Map unhealthyEventSources() { + return eventSourceManager.allEventSources().entrySet().stream() + .filter(e -> e.getValue().getStatus() == Status.UNHEALTHY) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public Map informerEventSourceHealthIndicators() { + return eventSourceManager.allEventSources().entrySet().stream() + .filter(e -> e.getValue() instanceof InformerWrappingEventSourceHealthIndicator) + .collect(Collectors.toMap(Map.Entry::getKey, + e -> (InformerWrappingEventSourceHealthIndicator) e.getValue())); + + } + + /** + * @return Map with event sources that wraps an informer. Thus, either a + * {@link io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource} + * or an + * {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}. + */ + public Map unhealthyInformerEventSourceHealthIndicators() { + return eventSourceManager.allEventSources().entrySet().stream() + .filter(e -> e.getValue().getStatus() == Status.UNHEALTHY) + .filter(e -> e.getValue() instanceof InformerWrappingEventSourceHealthIndicator) + .collect(Collectors.toMap(Map.Entry::getKey, + e -> (InformerWrappingEventSourceHealthIndicator) e.getValue())); + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/EventSourceHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/EventSourceHealthIndicator.java new file mode 100644 index 0000000000..e44fcb5b72 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/EventSourceHealthIndicator.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.health; + +public interface EventSourceHealthIndicator { + + Status getStatus(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java new file mode 100644 index 0000000000..afd8b61bed --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.health; + +public interface InformerHealthIndicator extends EventSourceHealthIndicator { + + boolean hasSynced(); + + boolean isWatching(); + + boolean isRunning(); + + @Override + default Status getStatus() { + return isRunning() && hasSynced() && isWatching() ? Status.HEALTHY : Status.UNHEALTHY; + } + + String getTargetNamespace(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java new file mode 100644 index 0000000000..5a603ad321 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerWrappingEventSourceHealthIndicator.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.health; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; + +public interface InformerWrappingEventSourceHealthIndicator + extends EventSourceHealthIndicator { + + Map informerHealthIndicators(); + + @Override + default Status getStatus() { + var nonUp = informerHealthIndicators().values().stream() + .filter(i -> i.getStatus() != Status.HEALTHY).findAny(); + + return nonUp.isPresent() ? Status.UNHEALTHY : Status.HEALTHY; + } + + ResourceConfiguration getInformerConfiguration(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/Status.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/Status.java new file mode 100644 index 0000000000..d3a300b7d8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/Status.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.health; + +public enum Status { + + HEALTHY, UNHEALTHY, + /** + * For event sources where it cannot be determined if it is healthy ot not. + */ + UNKNOWN + +} 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 2d9e6b4007..8753e0862b 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 @@ -39,7 +39,8 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext; -import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; +import io.javaoperatorsdk.operator.health.ControllerHealthInfo; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow; import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; import io.javaoperatorsdk.operator.processing.event.EventProcessor; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; @@ -63,10 +64,11 @@ public class Controller

private final boolean contextInitializer; private final boolean isCleaner; private final Metrics metrics; - private final ManagedWorkflow

managedWorkflow; + private final Workflow

managedWorkflow; private final GroupVersionKind associatedGVK; private final EventProcessor

eventProcessor; + private final ControllerHealthInfo controllerHealthInfo; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -81,16 +83,18 @@ public Controller(Reconciler

reconciler, this.metrics = Optional.ofNullable(configurationService.getMetrics()).orElse(Metrics.NOOP); contextInitializer = reconciler instanceof ContextInitializer; isCleaner = reconciler instanceof Cleaner; - managedWorkflow = configurationService.getWorkflowFactory().workflowFor(configuration); - managedWorkflow.resolve(kubernetesClient, configuration.getDependentResources()); + + final var managed = configurationService.getWorkflowFactory().workflowFor(configuration); + managedWorkflow = managed.resolve(kubernetesClient, configuration); eventSourceManager = new EventSourceManager<>(this); eventProcessor = new EventProcessor<>(eventSourceManager); eventSourceManager.postProcessDefaultEventSourcesAfterProcessorInitializer(); - + controllerHealthInfo = new ControllerHealthInfo(eventSourceManager); final var context = new EventSourceContext<>( eventSourceManager.getControllerResourceEventSource(), configuration, kubernetesClient); initAndRegisterEventSources(context); + ConfigurationServiceProvider.instance().getMetrics().controllerRegistered(this); } @Override @@ -132,7 +136,7 @@ public Map metadata() { @Override public UpdateControl

execute() throws Exception { initContextIfNeeded(resource, context); - if (!managedWorkflow.isEmptyWorkflow()) { + if (!managedWorkflow.isEmpty()) { var res = managedWorkflow.reconcile(resource, context); ((DefaultManagedDependentResourceContext) context.managedDependentResourceContext()) .setWorkflowExecutionResult(res); @@ -177,7 +181,7 @@ public Map metadata() { public DeleteControl execute() { initContextIfNeeded(resource, context); WorkflowCleanupResult workflowCleanupResult = null; - if (managedWorkflow.isCleaner()) { + if (managedWorkflow.hasCleaner()) { workflowCleanupResult = managedWorkflow.cleanup(resource, context); ((DefaultManagedDependentResourceContext) context.managedDependentResourceContext()) .setWorkflowCleanupResult(workflowCleanupResult); @@ -228,13 +232,13 @@ public void initAndRegisterEventSources(EventSourceContext

context) { final var dependentResourcesByName = managedWorkflow.getDependentResourcesByName(); final var size = dependentResourcesByName.size(); if (size > 0) { - dependentResourcesByName.forEach((key, value) -> { - if (value instanceof EventSourceProvider) { - final var provider = (EventSourceProvider) value; + dependentResourcesByName.forEach((key, dependentResource) -> { + if (dependentResource instanceof EventSourceProvider) { + final var provider = (EventSourceProvider) dependentResource; final var source = provider.initEventSource(context); eventSourceManager.registerEventSource(key, source); } else { - Optional eventSource = value.eventSource(context); + Optional eventSource = dependentResource.eventSource(context); eventSource.ifPresent(es -> eventSourceManager.registerEventSource(key, es)); } }); @@ -289,6 +293,11 @@ public ControllerConfiguration

getConfiguration() { return configuration; } + @Override + public ControllerHealthInfo getControllerHealthInfo() { + return controllerHealthInfo; + } + public KubernetesClient getClient() { return kubernetesClient; } @@ -350,10 +359,13 @@ private void validateCRDWithLocalModelIfRequired(Class

resClass, String contr } public void changeNamespaces(Set namespaces) { - if (namespaces.contains(Constants.WATCH_ALL_NAMESPACES) - || namespaces.contains(WATCH_CURRENT_NAMESPACE)) { + if (namespaces.contains(WATCH_CURRENT_NAMESPACE)) { throw new OperatorException("Unexpected value in target namespaces: " + namespaces); } + if (namespaces.contains(Constants.WATCH_ALL_NAMESPACES) && namespaces.size() > 1) { + throw new OperatorException( + "Watching all namespaces, but additional specific namespace is present"); + } eventProcessor.stop(); eventSourceManager.changeNamespaces(namespaces); eventProcessor.start(); @@ -406,7 +418,7 @@ public synchronized void stop() { } public boolean useFinalizer() { - return isCleaner || managedWorkflow.isCleaner(); + return isCleaner || managedWorkflow.hasCleaner(); } public GroupVersionKind getAssociatedGroupVersionKind() { 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 dbfc34fe42..7a32938403 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,6 +1,6 @@ package io.javaoperatorsdk.operator.processing.dependent; -import java.util.*; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,6 +9,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; @@ -21,6 +22,7 @@ public abstract class AbstractDependentResource private final boolean creatable = this instanceof Creator; private final boolean updatable = this instanceof Updater; + private final boolean deletable = this instanceof Deleter; protected Creator creator; protected Updater updater; @@ -172,4 +174,9 @@ protected boolean isCreatable() { protected boolean isUpdatable() { return updatable; } + + @Override + public boolean isDeletable() { + return deletable; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java new file mode 100644 index 0000000000..a9a60f8e0a --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java @@ -0,0 +1,61 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.Arrays; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; +import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; + +public class KubernetesDependentConverter implements + ConfigurationConverter, KubernetesDependentResource> { + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public KubernetesDependentResourceConfig configFrom(KubernetesDependent configAnnotation, + ControllerConfiguration parentConfiguration, + Class> originatingClass) { + var namespaces = parentConfiguration.getNamespaces(); + var configuredNS = false; + String labelSelector = null; + OnAddFilter onAddFilter = null; + OnUpdateFilter onUpdateFilter = null; + OnDeleteFilter onDeleteFilter = null; + GenericFilter genericFilter = null; + ResourceDiscriminator resourceDiscriminator = null; + if (configAnnotation != null) { + if (!Arrays.equals(KubernetesDependent.DEFAULT_NAMESPACES, configAnnotation.namespaces())) { + namespaces = Set.of(configAnnotation.namespaces()); + configuredNS = true; + } + + final var fromAnnotation = configAnnotation.labelSelector(); + labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation; + + final var context = + Utils.contextFor(parentConfiguration, originatingClass, + configAnnotation.annotationType()); + onAddFilter = Utils.instantiate(configAnnotation.onAddFilter(), OnAddFilter.class, context); + onUpdateFilter = + Utils.instantiate(configAnnotation.onUpdateFilter(), OnUpdateFilter.class, context); + onDeleteFilter = + Utils.instantiate(configAnnotation.onDeleteFilter(), OnDeleteFilter.class, context); + genericFilter = + Utils.instantiate(configAnnotation.genericFilter(), GenericFilter.class, context); + + resourceDiscriminator = + Utils.instantiate(configAnnotation.resourceDiscriminator(), ResourceDiscriminator.class, + context); + } + + return new KubernetesDependentResourceConfig(namespaces, labelSelector, configuredNS, + resourceDiscriminator, onAddFilter, onUpdateFilter, onDeleteFilter, genericFilter); + } +} 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 5631536da5..a8bfcb798c 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 @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; -import java.util.Arrays; import java.util.HashMap; import java.util.Optional; import java.util.Set; @@ -12,35 +11,30 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.config.dependent.Configured; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.AbstractEventSourceHolderDependentResource; import io.javaoperatorsdk.operator.processing.dependent.Matcher; import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; -import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; -import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @Ignore -@SuppressWarnings("rawtypes") +@Configured(by = KubernetesDependent.class, with = KubernetesDependentResourceConfig.class, + converter = KubernetesDependentConverter.class) public abstract class KubernetesDependentResource extends AbstractEventSourceHolderDependentResource> implements KubernetesClientAware, - AnnotationDependentResourceConfigurator> { + DependentResourceConfigurator> { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); @@ -250,46 +244,6 @@ private void cleanupAfterEventFiltering(ResourceID resourceID) { .cleanupOnCreateOrUpdateEventFiltering(resourceID); } - @Override - @SuppressWarnings("unchecked") - public KubernetesDependentResourceConfig configFrom(KubernetesDependent kubeDependent, - ControllerConfiguration parentConfiguration) { - var namespaces = parentConfiguration.getNamespaces(); - var configuredNS = false; - String labelSelector = null; - OnAddFilter onAddFilter = null; - OnUpdateFilter onUpdateFilter = null; - OnDeleteFilter onDeleteFilter = null; - GenericFilter genericFilter = null; - ResourceDiscriminator resourceDiscriminator = 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; - - final var context = - Utils.contextFor(parentConfiguration, getClass(), kubeDependent.annotationType()); - onAddFilter = Utils.instantiate(kubeDependent.onAddFilter(), OnAddFilter.class, context); - onUpdateFilter = - Utils.instantiate(kubeDependent.onUpdateFilter(), OnUpdateFilter.class, context); - onDeleteFilter = - Utils.instantiate(kubeDependent.onDeleteFilter(), OnDeleteFilter.class, context); - genericFilter = - Utils.instantiate(kubeDependent.genericFilter(), GenericFilter.class, context); - - resourceDiscriminator = - Utils.instantiate(kubeDependent.resourceDiscriminator(), ResourceDiscriminator.class, - context); - } - - return new KubernetesDependentResourceConfig(namespaces, labelSelector, configuredNS, - resourceDiscriminator, onAddFilter, onUpdateFilter, onDeleteFilter, genericFilter); - } - @Override public Optional> configuration() { return Optional.ofNullable(kubernetesDependentResourceConfig); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractDependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractDependentResourceNode.java deleted file mode 100644 index 22e368249a..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractDependentResourceNode.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.workflow; - -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; - -@SuppressWarnings("rawtypes") -abstract class AbstractDependentResourceNode - implements DependentResourceNode { - - private final List dependsOn = new LinkedList<>(); - private final List parents = new LinkedList<>(); - private final String name; - private Condition reconcilePrecondition; - private Condition deletePostcondition; - private Condition readyPostcondition; - private DependentResource dependentResource; - - protected AbstractDependentResourceNode(String name) { - this.name = name; - } - - @Override - public List getDependsOn() { - return dependsOn; - } - - @Override - public void addParent(DependentResourceNode parent) { - parents.add(parent); - } - - @Override - public void addDependsOnRelation(DependentResourceNode node) { - node.addParent(this); - dependsOn.add(node); - } - - @Override - public List getParents() { - return parents; - } - - @Override - public String getName() { - return name; - } - - @Override - public Optional> getReconcilePrecondition() { - return Optional.ofNullable(reconcilePrecondition); - } - - @Override - public Optional> getDeletePostcondition() { - return Optional.ofNullable(deletePostcondition); - } - - public void setReconcilePrecondition(Condition reconcilePrecondition) { - this.reconcilePrecondition = reconcilePrecondition; - } - - public void setDeletePostcondition(Condition cleanupCondition) { - this.deletePostcondition = cleanupCondition; - } - - @Override - public Optional> getReadyPostcondition() { - return Optional.ofNullable(readyPostcondition); - } - - public void setReadyPostcondition(Condition readyPostcondition) { - this.readyPostcondition = readyPostcondition; - } - - public DependentResource getDependentResource() { - return dependentResource; - } - - public void setDependentResource(DependentResource dependentResource) { - this.dependentResource = dependentResource; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AbstractDependentResourceNode that = (AbstractDependentResourceNode) o; - return name.equals(that.name); - } - - @Override - public int hashCode() { - return name.hashCode(); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java index 354975ebad..76db792468 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java @@ -87,7 +87,7 @@ protected boolean isInError(DependentResourceNode dependentResourceNode) { protected Map getErroredDependents() { return exceptionsDuringExecution.entrySet().stream() .collect( - Collectors.toMap(e -> workflow.getDependentResourceFor(e.getKey()), Entry::getValue)); + Collectors.toMap(e -> e.getKey().getDependentResource(), Entry::getValue)); } protected synchronized void handleNodeExecutionFinish( @@ -99,11 +99,6 @@ protected synchronized void handleNodeExecutionFinish( } } - @SuppressWarnings("unchecked") - protected DependentResource getDependentResourceFor(DependentResourceNode drn) { - return (DependentResource) workflow.getDependentResourceFor(drn); - } - protected boolean isConditionMet(Optional> condition, DependentResource dependentResource) { return condition.map(c -> c.isMet(primary, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultDependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultDependentResourceNode.java deleted file mode 100644 index d0d844ea9c..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultDependentResourceNode.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.workflow; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; - -class DefaultDependentResourceNode - extends AbstractDependentResourceNode { - - public DefaultDependentResourceNode(DependentResource dependentResource) { - this(dependentResource, null, null); - } - - public DefaultDependentResourceNode(DependentResource dependentResource, - Condition reconcilePrecondition, Condition deletePostcondition) { - super(getNameFor(dependentResource)); - setDependentResource(dependentResource); - setReconcilePrecondition(reconcilePrecondition); - setDeletePostcondition(deletePostcondition); - } - - @SuppressWarnings("rawtypes") - static String getNameFor(DependentResource dependentResource) { - return DependentResource.defaultNameFor(dependentResource.getClass()) + "#" - + dependentResource.hashCode(); - } - - @Override - public String toString() { - return "DependentResourceNode{" + getDependentResource() + '}'; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java index 3096f1d4fb..55b9ee6edd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java @@ -1,65 +1,126 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; + +import static io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow.THROW_EXCEPTION_AUTOMATICALLY_DEFAULT; @SuppressWarnings("rawtypes") public class DefaultManagedWorkflow

implements ManagedWorkflow

{ - private final Workflow

workflow; - private final boolean isEmptyWorkflow; - private boolean resolved; + private final Set topLevelResources; + private final Set bottomLevelResources; + private final List> orderedSpecs; + private final boolean hasCleaner; - DefaultManagedWorkflow(List dependentResourceSpecs, Workflow

workflow) { - isEmptyWorkflow = dependentResourceSpecs.isEmpty(); - this.workflow = workflow; + protected DefaultManagedWorkflow(List> orderedSpecs, + boolean hasCleaner) { + this.hasCleaner = hasCleaner; + topLevelResources = new HashSet<>(orderedSpecs.size()); + bottomLevelResources = orderedSpecs.stream() + .map(DependentResourceSpec::getName) + .collect(Collectors.toSet()); + this.orderedSpecs = orderedSpecs; + orderedSpecs.forEach(spec -> { + // add cycle detection? + if (spec.getDependsOn().isEmpty()) { + topLevelResources.add(spec.getName()); + } else { + for (String dependsOn : spec.getDependsOn()) { + bottomLevelResources.remove(dependsOn); + } + } + }); } - public WorkflowReconcileResult reconcile(P primary, Context

context) { - checkIfResolved(); - return workflow.reconcile(primary, context); + @Override + @SuppressWarnings("unused") + public List> getOrderedSpecs() { + return orderedSpecs; } - public WorkflowCleanupResult cleanup(P primary, Context

context) { - checkIfResolved(); - return workflow.cleanup(primary, context); + protected Set getTopLevelResources() { + return topLevelResources; } - public boolean isCleaner() { - return workflow.hasCleaner(); + protected Set getBottomLevelResources() { + return bottomLevelResources; } - public boolean isEmptyWorkflow() { - return isEmptyWorkflow; + List nodeNames() { + return orderedSpecs.stream().map(DependentResourceSpec::getName).collect(Collectors.toList()); + } + + @Override + public boolean hasCleaner() { + return hasCleaner; } - public Map getDependentResourcesByName() { - checkIfResolved(); - final var nodes = workflow.nodes(); - final var result = new HashMap(nodes.size()); - nodes.forEach((key, drn) -> result.put(key, workflow.getDependentResourceFor(drn))); - return result; + @Override + public boolean isEmpty() { + return orderedSpecs.isEmpty(); } @Override - public ManagedWorkflow

resolve(KubernetesClient client, List specs) { - if (!resolved) { - workflow.resolve(client, specs); - resolved = true; + @SuppressWarnings("unchecked") + public Workflow

resolve(KubernetesClient client, + ControllerConfiguration

configuration) { + final var alreadyResolved = new HashMap(orderedSpecs.size()); + for (DependentResourceSpec spec : orderedSpecs) { + final var node = new DependentResourceNode(spec.getName(), + spec.getReconcileCondition(), + spec.getDeletePostCondition(), + spec.getReadyCondition(), + resolve(spec, client, configuration)); + alreadyResolved.put(node.getName(), node); + spec.getDependsOn() + .forEach(depend -> node.addDependsOnRelation(alreadyResolved.get((String) depend))); } - return this; + + final var bottom = + bottomLevelResources.stream().map(alreadyResolved::get).collect(Collectors.toSet()); + final var top = + topLevelResources.stream().map(alreadyResolved::get).collect(Collectors.toSet()); + return new DefaultWorkflow<>(alreadyResolved, bottom, top, + THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, hasCleaner); } - private void checkIfResolved() { - if (!resolved) { - throw new IllegalStateException("resolve should be called before"); + @SuppressWarnings({"rawtypes", "unchecked"}) + private DependentResource resolve(DependentResourceSpec spec, + KubernetesClient client, + ControllerConfiguration

configuration) { + final DependentResource dependentResource = + ConfigurationServiceProvider.instance().dependentResourceFactory() + .createFrom(spec, configuration); + + if (dependentResource instanceof KubernetesClientAware) { + ((KubernetesClientAware) dependentResource).setKubernetesClient(client); } + + spec.getUseEventSourceWithName() + .ifPresent(esName -> { + if (dependentResource instanceof EventSourceReferencer) { + ((EventSourceReferencer) dependentResource).useEventSourceWithName(esName); + } else { + throw new IllegalStateException( + "DependentResource " + spec + " wants to use EventSource named " + esName + + " but doesn't implement support for this feature by implementing " + + EventSourceReferencer.class.getSimpleName()); + } + }); + + return dependentResource; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java new file mode 100644 index 0000000000..a151ff0947 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java @@ -0,0 +1,146 @@ +package io.javaoperatorsdk.operator.processing.dependent.workflow; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; + +/** + * Dependents definition: so if B depends on A, the B is dependent of A. + * + * @param

primary resource + */ +@SuppressWarnings("rawtypes") +public class DefaultWorkflow

implements Workflow

{ + + private final Map dependentResourceNodes; + private final Set topLevelResources; + private final Set bottomLevelResource; + + private final boolean throwExceptionAutomatically; + private final boolean hasCleaner; + + DefaultWorkflow(Set dependentResourceNodes) { + this(dependentResourceNodes, THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, false); + } + + DefaultWorkflow(Set dependentResourceNodes, + boolean throwExceptionAutomatically, + boolean hasCleaner) { + this.throwExceptionAutomatically = throwExceptionAutomatically; + this.hasCleaner = hasCleaner; + + if (dependentResourceNodes == null) { + this.topLevelResources = Collections.emptySet(); + this.bottomLevelResource = Collections.emptySet(); + this.dependentResourceNodes = Collections.emptyMap(); + } else { + this.topLevelResources = new HashSet<>(dependentResourceNodes.size()); + this.bottomLevelResource = new HashSet<>(dependentResourceNodes); + this.dependentResourceNodes = toMap(dependentResourceNodes); + } + } + + protected DefaultWorkflow(Map dependentResourceNodes, + Set bottomLevelResource, Set topLevelResources, + boolean throwExceptionAutomatically, + boolean hasCleaner) { + this.throwExceptionAutomatically = throwExceptionAutomatically; + this.hasCleaner = hasCleaner; + + this.topLevelResources = topLevelResources; + this.bottomLevelResource = bottomLevelResource; + this.dependentResourceNodes = dependentResourceNodes; + } + + @SuppressWarnings("unchecked") + private Map toMap( + Set dependentResourceNodes) { + return dependentResourceNodes.stream() + .peek(drn -> { + // add cycle detection? + if (drn.getDependsOn().isEmpty()) { + topLevelResources.add(drn); + } else { + for (DependentResourceNode dependsOn : (List) drn + .getDependsOn()) { + bottomLevelResource.remove(dependsOn); + } + } + }) + .collect(Collectors.toMap(DependentResourceNode::getName, Function.identity())); + } + + @Override + public WorkflowReconcileResult reconcile(P primary, Context

context) { + WorkflowReconcileExecutor

workflowReconcileExecutor = + new WorkflowReconcileExecutor<>(this, primary, context); + var result = workflowReconcileExecutor.reconcile(); + if (throwExceptionAutomatically) { + result.throwAggregateExceptionIfErrorsPresent(); + } + return result; + } + + @Override + public WorkflowCleanupResult cleanup(P primary, Context

context) { + WorkflowCleanupExecutor

workflowCleanupExecutor = + new WorkflowCleanupExecutor<>(this, primary, context); + var result = workflowCleanupExecutor.cleanup(); + if (throwExceptionAutomatically) { + result.throwAggregateExceptionIfErrorsPresent(); + } + return result; + } + + @Override + public Set getTopLevelDependentResources() { + return topLevelResources; + } + + @Override + public Set getBottomLevelResource() { + return bottomLevelResource; + } + + @Override + public boolean hasCleaner() { + return hasCleaner; + } + + static boolean isDeletable(Class drClass) { + final var isDeleter = Deleter.class.isAssignableFrom(drClass); + if (!isDeleter) { + return false; + } + + if (KubernetesDependentResource.class.isAssignableFrom(drClass)) { + return !GarbageCollected.class.isAssignableFrom(drClass); + } + return true; + } + + @Override + public boolean isEmpty() { + return dependentResourceNodes.isEmpty(); + } + + @Override + public Map getDependentResourcesByName() { + final var resources = new HashMap(dependentResourceNodes.size()); + dependentResourceNodes + .forEach((name, node) -> resources.put(name, node.getDependentResource())); + return resources; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java index 2c069bf999..6d82c8cb17 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java @@ -1,30 +1,113 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; +import java.util.LinkedList; import java.util.List; import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @SuppressWarnings("rawtypes") -public interface DependentResourceNode { +public class DependentResourceNode { - Optional> getReconcilePrecondition(); + private final List dependsOn = new LinkedList<>(); + private final List parents = new LinkedList<>(); + private final String name; + private Condition reconcilePrecondition; + private Condition deletePostcondition; + private Condition readyPostcondition; + private final DependentResource dependentResource; - Optional> getDeletePostcondition(); + DependentResourceNode(DependentResource dependentResource) { + this(getNameFor(dependentResource), null, null, null, dependentResource); + } - List getDependsOn(); + public DependentResourceNode(String name, Condition reconcilePrecondition, + Condition deletePostcondition, Condition readyPostcondition, + DependentResource dependentResource) { + this.name = name; + this.reconcilePrecondition = reconcilePrecondition; + this.deletePostcondition = deletePostcondition; + this.readyPostcondition = readyPostcondition; + this.dependentResource = dependentResource; + } - void addDependsOnRelation(DependentResourceNode node); + public List getDependsOn() { + return dependsOn; + } - Optional> getReadyPostcondition(); + void addParent(DependentResourceNode parent) { + parents.add(parent); + } - List getParents(); + void addDependsOnRelation(DependentResourceNode node) { + node.addParent(this); + dependsOn.add(node); + } - void addParent(DependentResourceNode parent); + public List getParents() { + return parents; + } - String getName(); + public String getName() { + return name; + } - default void resolve(KubernetesClient client, List dependentResources) {} + + public Optional> getReconcilePrecondition() { + return Optional.ofNullable(reconcilePrecondition); + } + + + public Optional> getDeletePostcondition() { + return Optional.ofNullable(deletePostcondition); + } + + void setReconcilePrecondition(Condition reconcilePrecondition) { + this.reconcilePrecondition = reconcilePrecondition; + } + + void setDeletePostcondition(Condition cleanupCondition) { + this.deletePostcondition = cleanupCondition; + } + + public Optional> getReadyPostcondition() { + return Optional.ofNullable(readyPostcondition); + } + + void setReadyPostcondition(Condition readyPostcondition) { + this.readyPostcondition = readyPostcondition; + } + + public DependentResource getDependentResource() { + return dependentResource; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DependentResourceNode that = (DependentResourceNode) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @SuppressWarnings("rawtypes") + static String getNameFor(DependentResource dependentResource) { + return DependentResource.defaultNameFor(dependentResource.getClass()) + "#" + + dependentResource.hashCode(); + } + + @Override + public String toString() { + return "DependentResourceNode{" + getDependentResource() + '}'; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java index de2b634b5a..2de3075818 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java @@ -2,59 +2,26 @@ import java.util.Collections; import java.util.List; -import java.util.Map; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -@SuppressWarnings("rawtypes") public interface ManagedWorkflow

{ - ManagedWorkflow noOpWorkflow = new ManagedWorkflow() { - @Override - public WorkflowReconcileResult reconcile(HasMetadata primary, Context context) { - throw new IllegalStateException("Shouldn't be called"); - } + @SuppressWarnings("unused") + default List> getOrderedSpecs() { + return Collections.emptyList(); + } - @Override - public WorkflowCleanupResult cleanup(HasMetadata primary, Context context) { - throw new IllegalStateException("Shouldn't be called"); - } + default boolean hasCleaner() { + return false; + } - @Override - public boolean isCleaner() { - return false; - } + default boolean isEmpty() { + return true; + } - @Override - public boolean isEmptyWorkflow() { - return true; - } - - @Override - public Map getDependentResourcesByName() { - return Collections.emptyMap(); - } - - @Override - public ManagedWorkflow resolve(KubernetesClient client, List dependentResources) { - return this; - } - }; - - WorkflowReconcileResult reconcile(P primary, Context

context); - - WorkflowCleanupResult cleanup(P primary, Context

context); - - boolean isCleaner(); - - boolean isEmptyWorkflow(); - - Map getDependentResourcesByName(); - - ManagedWorkflow

resolve(KubernetesClient client, - List dependentResources); + Workflow

resolve(KubernetesClient client, ControllerConfiguration

configuration); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowFactory.java index 5b5135e95a..4923084de8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowFactory.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowFactory.java @@ -1,22 +1,35 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import static io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow.noOpWorkflow; - -public interface ManagedWorkflowFactory { +public interface ManagedWorkflowFactory> { @SuppressWarnings({"rawtypes", "unchecked"}) ManagedWorkflowFactory DEFAULT = (configuration) -> { final var dependentResourceSpecs = configuration.getDependentResources(); if (dependentResourceSpecs == null || dependentResourceSpecs.isEmpty()) { - return noOpWorkflow; + return new ManagedWorkflow() { + @Override + public boolean hasCleaner() { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public Workflow resolve(KubernetesClient client, ControllerConfiguration configuration) { + return new DefaultWorkflow(null); + } + }; } - return new DefaultManagedWorkflow(dependentResourceSpecs, - ManagedWorkflowSupport.instance().createWorkflow(dependentResourceSpecs)); + return ManagedWorkflowSupport.instance().createWorkflow(dependentResourceSpecs); }; @SuppressWarnings("rawtypes") - ManagedWorkflow workflowFor(ControllerConfiguration configuration); + ManagedWorkflow workflowFor(C configuration); } 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 9b11784443..430acfd784 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 @@ -46,27 +46,17 @@ public void checkForNameDuplication(List dependentResourc } } - public

Workflow

createWorkflow( + + public

ManagedWorkflow

createWorkflow( List dependentResourceSpecs) { - var orderedResourceSpecs = orderAndDetectCycles(dependentResourceSpecs); - final var alreadyCreated = new ArrayList(orderedResourceSpecs.size()); - final var nodes = orderedResourceSpecs.stream() - .map(spec -> createFrom(spec, alreadyCreated)) - .collect(Collectors.toSet()); - return new Workflow<>(nodes); + return createAsDefault(dependentResourceSpecs); } - private DependentResourceNode createFrom(DependentResourceSpec spec, - List alreadyCreated) { - final var node = new SpecDependentResourceNode<>(spec); - alreadyCreated.add(node); - spec.getDependsOn().forEach(depend -> { - final DependentResourceNode dependsOn = alreadyCreated.stream() - .filter(drn -> depend.equals(drn.getName())).findFirst() - .orElseThrow(); - node.addDependsOnRelation(dependsOn); - }); - return node; +

DefaultManagedWorkflow

createAsDefault( + List dependentResourceSpecs) { + final boolean[] cleanerHolder = {false}; + var orderedResourceSpecs = orderAndDetectCycles(dependentResourceSpecs, cleanerHolder); + return new DefaultManagedWorkflow<>(orderedResourceSpecs, cleanerHolder[0]); } /** @@ -74,17 +64,22 @@ private DependentResourceNode createFrom(DependentResourceSpec spec, * @return top-bottom ordered resources that can be added safely to workflow * @throws OperatorException if there is a cycle in the dependencies */ - public List orderAndDetectCycles( - List dependentResourceSpecs) { + private List> orderAndDetectCycles( + List dependentResourceSpecs, boolean[] cleanerHolder) { final var drInfosByName = createDRInfos(dependentResourceSpecs); - final var orderedSpecs = new ArrayList(dependentResourceSpecs.size()); + final var orderedSpecs = + new ArrayList>(dependentResourceSpecs.size()); final var alreadyVisited = new HashSet(); var toVisit = getTopDependentResources(dependentResourceSpecs); while (!toVisit.isEmpty()) { final var toVisitNext = new HashSet(); toVisit.forEach(dr -> { + if (cleanerHolder != null) { + cleanerHolder[0] = + cleanerHolder[0] || DefaultWorkflow.isDeletable(dr.getDependentResourceClass()); + } final var name = dr.getName(); var drInfo = drInfosByName.get(name); if (drInfo != null) { @@ -108,6 +103,16 @@ public List orderAndDetectCycles( return orderedSpecs; } + /** + * @param dependentResourceSpecs list of specs + * @return top-bottom ordered resources that can be added safely to workflow + * @throws OperatorException if there is a cycle in the dependencies + */ + public List> orderAndDetectCycles( + List dependentResourceSpecs) { + return orderAndDetectCycles(dependentResourceSpecs, null); + } + private static class DRInfo { private final DependentResourceSpec spec; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java index 393e3d2e2e..0067c55321 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/NodeExecutor.java @@ -17,7 +17,7 @@ protected NodeExecutor(DependentResourceNode dependentResourceNode, @Override public void run() { try { - var dependentResource = workflowExecutor.getDependentResourceFor(dependentResourceNode); + var dependentResource = dependentResourceNode.getDependentResource(); doRun(dependentResourceNode, dependentResource); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java deleted file mode 100644 index ae8ed282c6..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/SpecDependentResourceNode.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.workflow; - -import java.util.List; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; - -class SpecDependentResourceNode - extends AbstractDependentResourceNode { - @SuppressWarnings("unchecked") - public SpecDependentResourceNode(DependentResourceSpec spec) { - super(spec.getName()); - setReadyPostcondition(spec.getReadyCondition()); - setDeletePostcondition(spec.getDeletePostCondition()); - setReconcilePrecondition(spec.getReconcileCondition()); - } - - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public void resolve(KubernetesClient client, List dependentResources) { - final var spec = dependentResources.stream() - .filter(drs -> drs.getName().equals(getName())) - .findFirst().orElseThrow(); - - final DependentResource dependentResource = spec.getDependentResource(); - - if (dependentResource instanceof KubernetesClientAware) { - ((KubernetesClientAware) dependentResource).setKubernetesClient(client); - } - - spec.getUseEventSourceWithName() - .ifPresent(esName -> { - final var name = (String) esName; - if (dependentResource instanceof EventSourceReferencer) { - ((EventSourceReferencer) dependentResource).useEventSourceWithName(name); - } else { - throw new IllegalStateException( - "DependentResource " + spec + " wants to use EventSource named " + name - + " but doesn't implement support for this feature by implementing " - + EventSourceReferencer.class.getSimpleName()); - } - }); - - setDependentResource(dependentResource); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java index 07163ab8c5..c06f17b7d8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java @@ -1,148 +1,45 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; +import java.util.Collections; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.function.Function; -import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -/** - * Dependents definition: so if B depends on A, the B is dependent of A. - * - * @param

primary resource - */ -@SuppressWarnings("rawtypes") -public class Workflow

{ +public interface Workflow

{ - public static final boolean THROW_EXCEPTION_AUTOMATICALLY_DEFAULT = true; + boolean THROW_EXCEPTION_AUTOMATICALLY_DEFAULT = true; - private final Map dependentResourceNodes; - private final Set topLevelResources = new HashSet<>(); - private final Set bottomLevelResource = new HashSet<>(); - - private final boolean throwExceptionAutomatically; - // it's "global" executor service shared between multiple reconciliations running parallel - private final ExecutorService executorService; - private boolean resolved; - private boolean hasCleaner; - - Workflow(Set dependentResourceNodes) { - this(dependentResourceNodes, ExecutorServiceManager.instance().workflowExecutorService(), - THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, false, false); - } - - Workflow(Set dependentResourceNodes, - ExecutorService executorService, boolean throwExceptionAutomatically, boolean resolved, - boolean hasCleaner) { - this.executorService = executorService; - this.dependentResourceNodes = dependentResourceNodes.stream() - .collect(Collectors.toMap(DependentResourceNode::getName, Function.identity())); - this.throwExceptionAutomatically = throwExceptionAutomatically; - this.resolved = resolved; - this.hasCleaner = hasCleaner; - preprocessForReconcile(); - } - - public DependentResource getDependentResourceFor(DependentResourceNode node) { - throwIfUnresolved(); - return dependentResource(node); - } - - private DependentResource dependentResource(DependentResourceNode node) { - return ((AbstractDependentResourceNode) dependentResourceNodes.get(node.getName())) - .getDependentResource(); - } - - private void throwIfUnresolved() { - if (!resolved) { - throw new IllegalStateException( - "Should call resolved before trying to access DependentResources"); - } - } - - public WorkflowReconcileResult reconcile(P primary, Context

context) { - throwIfUnresolved(); - WorkflowReconcileExecutor

workflowReconcileExecutor = - new WorkflowReconcileExecutor<>(this, primary, context); - var result = workflowReconcileExecutor.reconcile(); - if (throwExceptionAutomatically) { - result.throwAggregateExceptionIfErrorsPresent(); - } - return result; - } - - public WorkflowCleanupResult cleanup(P primary, Context

context) { - throwIfUnresolved(); - WorkflowCleanupExecutor

workflowCleanupExecutor = - new WorkflowCleanupExecutor<>(this, primary, context); - var result = workflowCleanupExecutor.cleanup(); - if (throwExceptionAutomatically) { - result.throwAggregateExceptionIfErrorsPresent(); - } - return result; - } - - // add cycle detection? - @SuppressWarnings("unchecked") - private void preprocessForReconcile() { - final var nodes = new ArrayList<>(dependentResourceNodes.values()); - bottomLevelResource.addAll(nodes); - for (DependentResourceNode node : nodes) { - if (node.getDependsOn().isEmpty()) { - topLevelResources.add(node); - } else { - for (DependentResourceNode dependsOn : node.getDependsOn()) { - bottomLevelResource.remove(dependsOn); - } - } - } + default WorkflowReconcileResult reconcile(P primary, Context

context) { + throw new UnsupportedOperationException("Implement this"); } - Set getTopLevelDependentResources() { - return topLevelResources; + default WorkflowCleanupResult cleanup(P primary, Context

context) { + throw new UnsupportedOperationException("Implement this"); } - Set getBottomLevelResource() { - return bottomLevelResource; + @SuppressWarnings("rawtypes") + default Set getTopLevelDependentResources() { + return Collections.emptySet(); } - ExecutorService getExecutorService() { - return executorService; + @SuppressWarnings("rawtypes") + default Set getBottomLevelResource() { + return Collections.emptySet(); } - Map nodes() { - return dependentResourceNodes; + default boolean hasCleaner() { + return false; } - @SuppressWarnings("unchecked") - void resolve(KubernetesClient client, List dependentResources) { - if (!resolved) { - final boolean[] cleanerHolder = {false}; - dependentResourceNodes.values() - .forEach(drn -> { - drn.resolve(client, dependentResources); - final var dr = dependentResource(drn); - if (dr.isDeletable()) { - cleanerHolder[0] = true; - } - }); - resolved = true; - hasCleaner = cleanerHolder[0]; - } + default boolean isEmpty() { + return true; } - boolean hasCleaner() { - throwIfUnresolved(); - return hasCleaner; + @SuppressWarnings("rawtypes") + default Map getDependentResourcesByName() { + return Collections.emptyMap(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java index 9f24ff3bd2..c9107a4266 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java @@ -5,11 +5,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import static io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow.THROW_EXCEPTION_AUTOMATICALLY_DEFAULT; @@ -17,14 +14,14 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public class WorkflowBuilder

{ - private final Map> dependentResourceNodes = + private final Map> dependentResourceNodes = new HashMap<>(); private boolean throwExceptionAutomatically = THROW_EXCEPTION_AUTOMATICALLY_DEFAULT; - private DefaultDependentResourceNode currentNode; + private DependentResourceNode currentNode; private boolean isCleaner = false; public WorkflowBuilder

addDependentResource(DependentResource dependentResource) { - currentNode = new DefaultDependentResourceNode<>(dependentResource); + currentNode = new DependentResourceNode<>(dependentResource); isCleaner = dependentResource.isDeletable(); final var name = currentNode.getName(); dependentResourceNodes.put(name, currentNode); @@ -64,7 +61,7 @@ public WorkflowBuilder

withDeletePostcondition(Condition deletePostcondition) DependentResourceNode getNodeByDependentResource(DependentResource dependentResource) { // first check by name final var node = - dependentResourceNodes.get(DefaultDependentResourceNode.getNameFor(dependentResource)); + dependentResourceNodes.get(DependentResourceNode.getNameFor(dependentResource)); if (node != null) { return node; } else { @@ -75,26 +72,13 @@ DependentResourceNode getNodeByDependentResource(DependentResource depende } } - public boolean isThrowExceptionAutomatically() { - return throwExceptionAutomatically; - } - public WorkflowBuilder

withThrowExceptionFurther(boolean throwExceptionFurther) { this.throwExceptionAutomatically = throwExceptionFurther; return this; } public Workflow

build() { - return build(ExecutorServiceManager.instance().workflowExecutorService()); - } - - public Workflow

build(int parallelism) { - return build(Executors.newFixedThreadPool(parallelism)); - } - - public Workflow

build(ExecutorService executorService) { - // workflow has been built from dependent resources so it is already resolved - return new Workflow(new HashSet<>(dependentResourceNodes.values()), executorService, - throwExceptionAutomatically, true, isCleaner); + return new DefaultWorkflow(new HashSet<>(dependentResourceNodes.values()), + throwExceptionAutomatically, isCleaner); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java index 12656c2a8a..3a65d8b112 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -52,7 +53,7 @@ private synchronized void handleCleanup(DependentResourceNode dependentResourceN return; } - Future nodeFuture = workflow.getExecutorService() + Future nodeFuture = ExecutorServiceManager.instance().workflowExecutorService() .submit(new CleanupExecutor<>(dependentResourceNode)); markAsExecuting(dependentResourceNode, nodeFuture); log.debug("Submitted for cleanup: {}", dependentResourceNode); @@ -116,10 +117,10 @@ private boolean hasErroredDependent(DependentResourceNode dependentResourceNode) private WorkflowCleanupResult createCleanupResult() { final var erroredDependents = getErroredDependents(); final var postConditionNotMet = postDeleteConditionNotMet.stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList()); final var deleteCalled = this.deleteCalled.stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList()); return new WorkflowCleanupResult(erroredDependents, postConditionNotMet, deleteCalled); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java index 98abe41977..773bb0332f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -63,11 +64,11 @@ private synchronized void handleReconcile(DependentResourceNode depend } boolean reconcileConditionMet = isConditionMet(dependentResourceNode.getReconcilePrecondition(), - getDependentResourceFor(dependentResourceNode)); + dependentResourceNode.getDependentResource()); if (!reconcileConditionMet) { handleReconcileConditionNotMet(dependentResourceNode); } else { - Future nodeFuture = workflow.getExecutorService() + Future nodeFuture = ExecutorServiceManager.instance().workflowExecutorService() .submit(new NodeReconcileExecutor(dependentResourceNode)); markAsExecuting(dependentResourceNode, nodeFuture); log.debug("Submitted to reconcile: {}", dependentResourceNode); @@ -85,7 +86,7 @@ private synchronized void handleDelete(DependentResourceNode dependentResourceNo return; } - Future nodeFuture = workflow.getExecutorService() + Future nodeFuture = ExecutorServiceManager.instance().workflowExecutorService() .submit(new NodeDeleteExecutor(dependentResourceNode)); markAsExecuting(dependentResourceNode, nodeFuture); log.debug("Submitted to delete: {}", dependentResourceNode); @@ -214,10 +215,10 @@ private boolean hasErroredParent(DependentResourceNode dependentResourceNo private WorkflowReconcileResult createReconcileResult() { return new WorkflowReconcileResult( reconciled.stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList()), notReady.stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList()), getErroredDependents(), reconcileResults); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index e7a600cf2c..59962e39f1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; import java.time.Duration; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; @@ -18,7 +18,6 @@ import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; @@ -32,25 +31,25 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -public class EventProcessor implements EventHandler, LifecycleAware { +public class EventProcessor

implements EventHandler, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventProcessor.class); private static final long MINIMAL_RATE_LIMIT_RESCHEDULE_DURATION = 50; private volatile boolean running; private final ControllerConfiguration controllerConfiguration; - private final ReconciliationDispatcher reconciliationDispatcher; + private final ReconciliationDispatcher

reconciliationDispatcher; private final Retry retry; private final ExecutorService executor; private final Metrics metrics; - private final Cache cache; - private final EventSourceManager eventSourceManager; + private final Cache

cache; + private final EventSourceManager

eventSourceManager; private final RateLimiter rateLimiter; private final ResourceStateManager resourceStateManager = new ResourceStateManager(); private final Map metricsMetadata; - public EventProcessor(EventSourceManager eventSourceManager) { + public EventProcessor(EventSourceManager

eventSourceManager) { this( eventSourceManager.getController().getConfiguration(), eventSourceManager.getControllerResourceEventSource(), @@ -63,8 +62,8 @@ public EventProcessor(EventSourceManager eventSourceManager) { @SuppressWarnings("rawtypes") EventProcessor( ControllerConfiguration controllerConfiguration, - ReconciliationDispatcher reconciliationDispatcher, - EventSourceManager eventSourceManager, + ReconciliationDispatcher

reconciliationDispatcher, + EventSourceManager

eventSourceManager, Metrics metrics) { this( controllerConfiguration, @@ -78,11 +77,11 @@ public EventProcessor(EventSourceManager eventSourceManager) { @SuppressWarnings({"rawtypes", "unchecked"}) private EventProcessor( ControllerConfiguration controllerConfiguration, - Cache cache, + Cache

cache, ExecutorService executor, - ReconciliationDispatcher reconciliationDispatcher, + ReconciliationDispatcher

reconciliationDispatcher, Metrics metrics, - EventSourceManager eventSourceManager) { + EventSourceManager

eventSourceManager) { this.controllerConfiguration = controllerConfiguration; this.running = false; this.executor = @@ -98,9 +97,10 @@ private EventProcessor( this.rateLimiter = controllerConfiguration.getRateLimiter(); metricsMetadata = Optional.ofNullable(eventSourceManager.getController()) - .map(Controller::getAssociatedGroupVersionKind) - .map(gvk -> Map.of(Constants.RESOURCE_GVK_KEY, (Object) gvk)) - .orElse(Collections.emptyMap()); + .map(c -> Map.of( + Constants.RESOURCE_GVK_KEY, c.getAssociatedGroupVersionKind(), + Constants.CONTROLLER_NAME, controllerConfiguration.getName())) + .orElse(new HashMap<>()); } @Override @@ -136,7 +136,7 @@ private void submitReconciliationExecution(ResourceState state) { try { boolean controllerUnderExecution = isControllerUnderExecution(state); final var resourceID = state.getId(); - Optional maybeLatest = cache.get(resourceID); + Optional

maybeLatest = cache.get(resourceID); maybeLatest.ifPresent(MDCUtils::addResourceInfo); if (!controllerUnderExecution && maybeLatest.isPresent()) { var rateLimit = state.getRateLimit(); @@ -151,11 +151,11 @@ private void submitReconciliationExecution(ResourceState state) { } state.setUnderProcessing(true); final var latest = maybeLatest.get(); - ExecutionScope executionScope = new ExecutionScope<>(latest, state.getRetry()); + ExecutionScope

executionScope = new ExecutionScope<>(state.getRetry()); state.unMarkEventReceived(); - metrics.reconcileCustomResource(resourceID, state.getRetry(), metricsMetadata); + metrics.reconcileCustomResource(latest, state.getRetry(), metricsMetadata); log.debug("Executing events for custom resource. Scope: {}", executionScope); - executor.execute(new ReconcilerExecutor(executionScope)); + executor.execute(new ReconcilerExecutor(resourceID, executionScope)); } else { log.debug( "Skipping executing controller for resource id: {}. Controller in execution: {}. Latest Resource present: {}", @@ -221,7 +221,7 @@ private void handleRateLimitedSubmission(ResourceID resourceID, Duration minimal } synchronized void eventProcessingFinished( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { + ExecutionScope

executionScope, PostExecutionControl

postExecutionControl) { if (!running) { return; } @@ -244,7 +244,7 @@ synchronized void eventProcessingFinished( return; } cleanupOnSuccessfulExecution(executionScope); - metrics.finishedReconciliation(resourceID, metricsMetadata); + metrics.finishedReconciliation(executionScope.getResource(), metricsMetadata); if (state.deleteEventPresent()) { cleanupForDeletedEvent(executionScope.getResourceID()); } else if (postExecutionControl.isFinalizerRemoved()) { @@ -253,12 +253,12 @@ synchronized void eventProcessingFinished( postExecutionControl .getUpdatedCustomResource() .ifPresent( - r -> { + p -> { if (!postExecutionControl.updateIsStatusPatch()) { eventSourceManager .getControllerResourceEventSource() .handleRecentResourceUpdate( - ResourceID.fromResource(r), r, executionScope.getResource()); + ResourceID.fromResource(p), p, executionScope.getResource()); } }); if (state.eventPresent()) { @@ -270,7 +270,7 @@ synchronized void eventProcessingFinished( } private void reScheduleExecutionIfInstructed( - PostExecutionControl postExecutionControl, R customResource) { + PostExecutionControl

postExecutionControl, P customResource) { postExecutionControl .getReScheduleDelay() @@ -281,7 +281,7 @@ private void reScheduleExecutionIfInstructed( }, () -> scheduleExecutionForMaxReconciliationInterval(customResource)); } - private void scheduleExecutionForMaxReconciliationInterval(R customResource) { + private void scheduleExecutionForMaxReconciliationInterval(P customResource) { this.controllerConfiguration .maxReconciliationInterval() .ifPresent(m -> { @@ -294,7 +294,7 @@ private void scheduleExecutionForMaxReconciliationInterval(R customResource) { }); } - TimerEventSource retryEventSource() { + TimerEventSource

retryEventSource() { return eventSourceManager.retryEventSource(); } @@ -304,7 +304,7 @@ TimerEventSource retryEventSource() { * according to the retry timing if there was an exception. */ private void handleRetryOnException( - ExecutionScope executionScope, Exception exception) { + ExecutionScope

executionScope, Exception exception) { final var state = getOrInitRetryExecution(executionScope); var resourceID = state.getId(); boolean eventPresent = state.eventPresent(); @@ -323,7 +323,7 @@ private void handleRetryOnException( "Scheduling timer event for retry with delay:{} for resource: {}", delay, resourceID); - metrics.failedReconciliation(resourceID, exception, metricsMetadata); + metrics.failedReconciliation(executionScope.getResource(), exception, metricsMetadata); retryEventSource().scheduleOnce(resourceID, delay); }, () -> { @@ -332,7 +332,7 @@ private void handleRetryOnException( }); } - private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) { + private void cleanupOnSuccessfulExecution(ExecutionScope

executionScope) { log.debug( "Cleanup for successful execution for resource: {}", getName(executionScope.getResource())); if (isRetryConfigured()) { @@ -341,7 +341,7 @@ private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) { retryEventSource().cancelOnceSchedule(executionScope.getResourceID()); } - private ResourceState getOrInitRetryExecution(ExecutionScope executionScope) { + private ResourceState getOrInitRetryExecution(ExecutionScope

executionScope) { final var state = resourceStateManager.getOrCreate(executionScope.getResourceID()); RetryExecution retryExecution = state.getRetry(); if (retryExecution == null) { @@ -387,10 +387,12 @@ private void handleAlreadyMarkedEvents() { } private class ReconcilerExecutor implements Runnable { - private final ExecutionScope executionScope; + private final ExecutionScope

executionScope; + private final ResourceID resourceID; - private ReconcilerExecutor(ExecutionScope executionScope) { + private ReconcilerExecutor(ResourceID resourceID, ExecutionScope

executionScope) { this.executionScope = executionScope; + this.resourceID = resourceID; } @Override @@ -399,12 +401,21 @@ public void run() { final var thread = Thread.currentThread(); final var name = thread.getName(); try { + var actualResource = cache.get(resourceID); + if (actualResource.isEmpty()) { + log.debug("Skipping execution; primary resource missing from cache: {}", + resourceID); + return; + } + actualResource.ifPresent(executionScope::setResource); MDCUtils.addResourceInfo(executionScope.getResource()); + metrics.reconciliationExecutionStarted(executionScope.getResource(), metricsMetadata); thread.setName("ReconcilerExecutor-" + controllerName() + "-" + thread.getId()); - PostExecutionControl postExecutionControl = + PostExecutionControl

postExecutionControl = reconciliationDispatcher.handleExecution(executionScope); eventProcessingFinished(executionScope, postExecutionControl); } finally { + metrics.reconciliationExecutionFinished(executionScope.getResource(), metricsMetadata); // restore original name thread.setName(name); MDCUtils.removeResourceInfo(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java index 8e9c981031..6a6aae471a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import java.util.*; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; @@ -198,6 +199,11 @@ public Set getRegisteredEventSources() { .collect(Collectors.toCollection(LinkedHashSet::new)); } + public Map allEventSources() { + return eventSources.allNamedEventSources().collect(Collectors.toMap(NamedEventSource::name, + NamedEventSource::original)); + } + @SuppressWarnings("unused") public Stream getNamedEventSourcesStream() { return eventSources.flatMappedSources(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java index c17843de2f..e140544586 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java @@ -49,6 +49,13 @@ public Stream additionalNamedEventSources() { flatMappedSources()); } + public Stream allNamedEventSources() { + return Stream.concat(Stream.of(namedControllerResourceEventSource(), + new NamedEventSource(retryAndRescheduleTimerEventSource, + RETRY_RESCHEDULE_TIMER_EVENT_SOURCE_NAME)), + flatMappedSources()); + } + Stream additionalEventSources() { return Stream.concat( Stream.of(retryEventSource()).filter(Objects::nonNull), diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java index e750c931c3..9621ea915b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java @@ -6,14 +6,18 @@ class ExecutionScope { // the latest custom resource from cache - private final R resource; + private R resource; private final RetryInfo retryInfo; - ExecutionScope(R resource, RetryInfo retryInfo) { - this.resource = resource; + ExecutionScope(RetryInfo retryInfo) { this.retryInfo = retryInfo; } + public ExecutionScope setResource(R resource) { + this.resource = resource; + return this; + } + public R getResource() { return resource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index 015a2f5c31..4a7ac68082 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; +import java.util.function.Function; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,6 +12,8 @@ import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; @@ -82,7 +86,7 @@ private PostExecutionControl

handleDispatch(ExecutionScope

executionScope) Context

context = new DefaultContext<>(executionScope.getRetryInfo(), controller, originalResource); if (markedForDeletion) { - return handleCleanup(resourceForExecution, context); + return handleCleanup(originalResource, resourceForExecution, context); } else { return handleReconcile(executionScope, resourceForExecution, originalResource, context); } @@ -109,7 +113,8 @@ private PostExecutionControl

handleReconcile( * finalizer add. This will make sure that the resources are not created before there is a * finalizer. */ - var updatedResource = updateCustomResourceWithFinalizer(originalResource); + var updatedResource = + updateCustomResourceWithFinalizer(resourceForExecution, originalResource); return PostExecutionControl.onlyFinalizerAdded(updatedResource); } else { try { @@ -276,7 +281,8 @@ private void updatePostExecutionControlWithReschedule( } - private PostExecutionControl

handleCleanup(P resource, Context

context) { + private PostExecutionControl

handleCleanup(P originalResource, P resource, + Context

context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), @@ -289,7 +295,8 @@ private PostExecutionControl

handleCleanup(P resource, Context

context) { // cleanup is finished, nothing left to done final var finalizerName = configuration().getFinalizerName(); if (deleteControl.isRemoveFinalizer() && resource.hasFinalizer(finalizerName)) { - P customResource = removeFinalizer(resource, finalizerName); + P customResource = conflictRetryingPatch(resource, originalResource, + r -> r.removeFinalizer(finalizerName)); return PostExecutionControl.customResourceFinalizerRemoved(customResource); } } @@ -304,11 +311,13 @@ private PostExecutionControl

handleCleanup(P resource, Context

context) { return postExecutionControl; } - private P updateCustomResourceWithFinalizer(P resource) { + private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalResource) { log.debug( - "Adding finalizer for resource: {} version: {}", getUID(resource), getVersion(resource)); - resource.addFinalizer(configuration().getFinalizerName()); - return customResourceFacade.updateResource(resource); + "Adding finalizer for resource: {} version: {}", getUID(originalResource), + getVersion(originalResource)); + + return conflictRetryingPatch(resourceForExecution, originalResource, + r -> r.addFinalizer(configuration().getFinalizerName())); } private P updateCustomResource(P resource) { @@ -321,20 +330,21 @@ ControllerConfiguration

configuration() { return controller.getConfiguration(); } - public P removeFinalizer(P resource, String finalizer) { + public P conflictRetryingPatch(P resource, P originalResource, + Function modificationFunction) { if (log.isDebugEnabled()) { log.debug("Removing finalizer on resource: {}", ResourceID.fromResource(resource)); } int retryIndex = 0; while (true) { try { - var removed = resource.removeFinalizer(finalizer); - if (!removed) { + var modified = modificationFunction.apply(resource); + if (Boolean.FALSE.equals(modified)) { return resource; } - return customResourceFacade.updateResource(resource); + return customResourceFacade.serverSideApplyLockResource(resource, originalResource); } catch (KubernetesClientException e) { - log.trace("Exception during finalizer removal for resource: {}", resource); + log.trace("Exception during patch for resource: {}", resource); retryIndex++; // only retry on conflict (HTTP 409), otherwise fail if (e.getCode() != 409) { @@ -343,7 +353,7 @@ public P removeFinalizer(P resource, String finalizer) { if (retryIndex >= MAX_FINALIZER_REMOVAL_RETRY) { throw new OperatorException( "Exceeded maximum (" + MAX_FINALIZER_REMOVAL_RETRY - + ") retry attempts to remove finalizer '" + finalizer + "' for resource " + + ") retry attempts to patch resource: " + ResourceID.fromResource(resource)); } resource = customResourceFacade.getResource(resource.getMetadata().getNamespace(), @@ -370,12 +380,18 @@ public R getResource(String namespace, String name) { } } + public R serverSideApplyLockResource(R resource, R originalResource) { + var patchContext = PatchContext.of(PatchType.SERVER_SIDE_APPLY); + patchContext.setForce(true); + return resource(originalResource).patch(patchContext, + resource); + } + public R updateResource(R resource) { log.debug( "Trying to replace resource {}, version: {}", getName(resource), resource.getMetadata().getResourceVersion()); - return resource(resource).lockResourceVersion(resource.getMetadata().getResourceVersion()) .replace(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java index 76c8ca164d..ec2783f797 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.processing.event.source; +import io.javaoperatorsdk.operator.health.EventSourceHealthIndicator; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -10,7 +12,7 @@ * your reconciler implement * {@link io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer}. */ -public interface EventSource extends LifecycleAware { +public interface EventSource extends LifecycleAware, EventSourceHealthIndicator { /** * Sets the {@link EventHandler} that is linked to your reconciler when this EventSource is @@ -23,4 +25,9 @@ public interface EventSource extends LifecycleAware { default EventSourceStartPriority priority() { return EventSourceStartPriority.DEFAULT; } + + @Override + default Status getStatus() { + return Status.UNKNOWN; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index ba7aa9f67f..94788e6706 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -1,10 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; @@ -25,15 +21,17 @@ import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACES; + public class InformerManager> implements LifecycleAware, IndexerResourceCache { - private static final String ALL_NAMESPACES_MAP_KEY = "allNamespaces"; private static final Logger log = LoggerFactory.getLogger(InformerManager.class); private final Map> sources = new ConcurrentHashMap<>(); @@ -60,17 +58,12 @@ void initSources(MixedOperation, Resource> clien final var labelSelector = configuration.getLabelSelector(); if (ResourceConfiguration.allNamespacesWatched(targetNamespaces)) { - final var filteredBySelectorClient = - client.inAnyNamespace().withLabelSelector(labelSelector); - final var source = - createEventSource(filteredBySelectorClient, eventHandler, ALL_NAMESPACES_MAP_KEY); + var source = createEventSourceForNamespace(WATCH_ALL_NAMESPACES); log.debug("Registered {} -> {} for any namespace", this, source); } else { targetNamespaces.forEach( ns -> { - final var source = - createEventSource(client.inNamespace(ns).withLabelSelector(labelSelector), - eventHandler, ns); + final var source = createEventSourceForNamespace(ns); log.debug("Registered {} -> {} for namespace: {}", this, source, ns); }); @@ -89,10 +82,7 @@ public void changeNamespaces(Set namespaces) { namespaces.forEach(ns -> { if (!sources.containsKey(ns)) { - final var source = - createEventSource( - client.inNamespace(ns).withLabelSelector(configuration.getLabelSelector()), - eventHandler, ns); + final InformerWrapper source = createEventSourceForNamespace(ns); source.addIndexers(this.indexers); source.start(); log.debug("Registered new {} -> {} for namespace: {}", this, source, @@ -102,13 +92,30 @@ public void changeNamespaces(Set namespaces) { } + private InformerWrapper createEventSourceForNamespace(String namespace) { + final InformerWrapper source; + if (namespace.equals(WATCH_ALL_NAMESPACES)) { + final var filteredBySelectorClient = + client.inAnyNamespace().withLabelSelector(configuration.getLabelSelector()); + source = createEventSource(filteredBySelectorClient, eventHandler, WATCH_ALL_NAMESPACES); + } else { + source = createEventSource( + client.inNamespace(namespace).withLabelSelector(configuration.getLabelSelector()), + eventHandler, namespace); + } + return source; + } private InformerWrapper createEventSource( FilterWatchListDeletable, Resource> filteredBySelectorClient, - ResourceEventHandler eventHandler, String key) { - var source = new InformerWrapper<>(filteredBySelectorClient.runnableInformer(0)); + ResourceEventHandler eventHandler, String namespaceIdentifier) { + var informer = filteredBySelectorClient.runnableInformer(0); + configuration.cachePruneFunction() + .ifPresent(f -> informer.itemStore(new TransformingItemStore<>(f))); + var source = + new InformerWrapper<>(informer, namespaceIdentifier); source.addEventHandler(eventHandler); - sources.put(key, source); + sources.put(namespaceIdentifier, source); return source; } @@ -135,7 +142,7 @@ public Stream list(Predicate predicate) { @Override public Stream list(String namespace, Predicate predicate) { if (isWatchingAllNamespaces()) { - return getSource(ALL_NAMESPACES_MAP_KEY) + return getSource(WATCH_ALL_NAMESPACES) .map(source -> source.list(namespace, predicate)) .orElseGet(Stream::empty); } else { @@ -147,7 +154,7 @@ public Stream list(String namespace, Predicate predicate) { @Override public Optional get(ResourceID resourceID) { - return getSource(resourceID.getNamespace().orElse(ALL_NAMESPACES_MAP_KEY)) + return getSource(resourceID.getNamespace().orElse(WATCH_ALL_NAMESPACES)) .flatMap(source -> source.get(resourceID)) .map(cloner::clone); } @@ -158,11 +165,11 @@ public Stream keys() { } private boolean isWatchingAllNamespaces() { - return sources.containsKey(ALL_NAMESPACES_MAP_KEY); + return sources.containsKey(WATCH_ALL_NAMESPACES); } private Optional> getSource(String namespace) { - namespace = isWatchingAllNamespaces() || namespace == null ? ALL_NAMESPACES_MAP_KEY : namespace; + namespace = isWatchingAllNamespaces() || namespace == null ? WATCH_ALL_NAMESPACES : namespace; return Optional.ofNullable(sources.get(namespace)); } @@ -187,4 +194,8 @@ public String toString() { + configuration.getEffectiveNamespaces() + (selector != null ? " selector: " + selector : ""); } + + public Map informerHealthIndicators() { + return Collections.unmodifiableMap(sources); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index de81815a96..17dc5cc969 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -21,20 +21,23 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.health.InformerHealthIndicator; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; class InformerWrapper - implements LifecycleAware, IndexerResourceCache { + implements LifecycleAware, IndexerResourceCache, InformerHealthIndicator { private static final Logger log = LoggerFactory.getLogger(InformerWrapper.class); private final SharedIndexInformer informer; private final Cache cache; + private final String namespaceIdentifier; - public InformerWrapper(SharedIndexInformer informer) { + public InformerWrapper(SharedIndexInformer informer, String namespaceIdentifier) { this.informer = informer; + this.namespaceIdentifier = namespaceIdentifier; this.cache = (Cache) informer.getStore(); } @@ -156,4 +159,24 @@ public String toString() { private String informerInfo() { return "InformerWrapper [" + versionedFullResourceName() + "]"; } + + @Override + public boolean hasSynced() { + return informer.hasSynced(); + } + + @Override + public boolean isWatching() { + return informer.isWatching(); + } + + @Override + public boolean isRunning() { + return informer.isRunning(); + } + + @Override + public String getTargetNamespace() { + return namespaceIdentifier; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 7fdf3c5e56..4286335644 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -19,6 +19,9 @@ import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; +import io.javaoperatorsdk.operator.health.InformerHealthIndicator; +import io.javaoperatorsdk.operator.health.InformerWrappingEventSourceHealthIndicator; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.Cache; @@ -29,17 +32,21 @@ public abstract class ManagedInformerEventSource implements ResourceEventHandler, Cache, IndexerResourceCache, RecentOperationCacheFiller, - NamespaceChangeable, Configurable { + NamespaceChangeable, InformerWrappingEventSourceHealthIndicator, Configurable { private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); - protected TemporaryResourceCache temporaryResourceCache = new TemporaryResourceCache<>(this); + protected TemporaryResourceCache temporaryResourceCache; protected InformerManager cache = new InformerManager<>(); + protected C configuration; protected ManagedInformerEventSource( MixedOperation, Resource> client, C configuration) { super(configuration.getResourceClass()); + temporaryResourceCache = new TemporaryResourceCache<>(this, + configuration.cachePruneFunction().orElse(null)); manager().initSources(client, configuration, this); + this.configuration = configuration; } @Override @@ -136,6 +143,21 @@ public Stream list(Predicate predicate) { return cache.list(predicate); } + @Override + public Map informerHealthIndicators() { + return cache.informerHealthIndicators(); + } + + @Override + public Status getStatus() { + return InformerWrappingEventSourceHealthIndicator.super.getStatus(); + } + + @Override + public ResourceConfiguration getInformerConfiguration() { + return configuration; + } + @Override public C configuration() { return manager().configuration(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index c0c041f3fb..0af2ec0b2f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -3,6 +3,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.UnaryOperator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,11 +35,14 @@ public class TemporaryResourceCache { private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); + private UnaryOperator cachePruneFunction; private final Map cache = new ConcurrentHashMap<>(); private final ManagedInformerEventSource managedInformerEventSource; - public TemporaryResourceCache(ManagedInformerEventSource managedInformerEventSource) { + public TemporaryResourceCache(ManagedInformerEventSource managedInformerEventSource, + UnaryOperator cachePruneFunction) { this.managedInformerEventSource = managedInformerEventSource; + this.cachePruneFunction = cachePruneFunction; } public synchronized void removeResourceFromCache(T resource) { @@ -46,14 +50,14 @@ public synchronized void removeResourceFromCache(T resource) { } public synchronized void unconditionallyCacheResource(T newResource) { - cache.put(ResourceID.fromResource(newResource), newResource); + putToCache(newResource, null); } public synchronized void putAddedResource(T newResource) { ResourceID resourceID = ResourceID.fromResource(newResource); if (managedInformerEventSource.get(resourceID).isEmpty()) { log.debug("Putting resource to cache with ID: {}", resourceID); - cache.put(resourceID, newResource); + putToCache(newResource, resourceID); } else { log.debug("Won't put resource into cache found already informer cache: {}", resourceID); } @@ -70,7 +74,7 @@ public synchronized void putUpdatedResource(T newResource, String previousResour if (informerCacheResource.get().getMetadata().getResourceVersion() .equals(previousResourceVersion)) { log.debug("Putting resource to temporal cache with id: {}", resourceId); - cache.put(resourceId, newResource); + putToCache(newResource, resourceId); } else { // if something is in cache it's surely obsolete now log.debug("Trying to remove an obsolete resource from cache for id: {}", resourceId); @@ -78,6 +82,13 @@ public synchronized void putUpdatedResource(T newResource, String previousResour } } + private void putToCache(T resource, ResourceID resourceID) { + if (cachePruneFunction != null) { + resource = cachePruneFunction.apply(resource); + } + cache.put(resourceID == null ? ResourceID.fromResource(resource) : resourceID, resource); + } + public synchronized Optional getResourceFromCache(ResourceID resourceID) { return Optional.ofNullable(cache.get(resourceID)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStore.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStore.java new file mode 100644 index 0000000000..60fdd32005 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStore.java @@ -0,0 +1,76 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.cache.Cache; +import io.fabric8.kubernetes.client.informers.cache.ItemStore; + +public class TransformingItemStore implements ItemStore { + + private Function keyFunction; + private UnaryOperator transformationFunction; + private ConcurrentHashMap store = new ConcurrentHashMap<>(); + + public TransformingItemStore(UnaryOperator transformationFunction) { + this(Cache::metaNamespaceKeyFunc, transformationFunction); + } + + public TransformingItemStore(Function keyFunction, + UnaryOperator transformationFunction) { + this.keyFunction = keyFunction; + this.transformationFunction = transformationFunction; + } + + @Override + public String getKey(R obj) { + return keyFunction.apply(obj); + } + + @Override + public R put(String key, R obj) { + var originalName = obj.getMetadata().getName(); + var originalNamespace = obj.getMetadata().getNamespace(); + var originalResourceVersion = obj.getMetadata().getResourceVersion(); + + var transformed = transformationFunction.apply(obj); + + transformed.getMetadata().setName(originalName); + transformed.getMetadata().setNamespace(originalNamespace); + transformed.getMetadata().setResourceVersion(originalResourceVersion); + return store.put(key, transformed); + } + + @Override + public R remove(String key) { + return store.remove(key); + } + + @Override + public Stream keySet() { + return store.keySet().stream(); + } + + @Override + public Stream values() { + return store.values().stream(); + } + + @Override + public R get(String key) { + return store.get(key); + } + + @Override + public int size() { + return store.size(); + } + + @Override + public boolean isFullState() { + return false; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java index 44bab7a624..8bf5c50fd4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java @@ -40,6 +40,7 @@ public class PerResourcePollingEventSource private final long period; private final Set fetchedForPrimaries = ConcurrentHashMap.newKeySet(); + public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, Cache

resourceCache, long period, Class resourceClass) { this(resourceFetcher, resourceCache, period, null, resourceClass, @@ -152,4 +153,5 @@ public void stop() throws OperatorException { super.stop(); timer.cancel(); } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java index 94efbf25aa..9ef889ecb6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java @@ -1,12 +1,14 @@ package io.javaoperatorsdk.operator.processing.event.source.polling; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CacheKeyMapper; import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; @@ -45,6 +47,7 @@ public class PollingEventSource private final Timer timer = new Timer(); private final GenericResourceFetcher genericResourceFetcher; private final long period; + private final AtomicBoolean healthy = new AtomicBoolean(true); public PollingEventSource( GenericResourceFetcher supplier, @@ -73,11 +76,17 @@ public void start() throws OperatorException { new TimerTask() { @Override public void run() { - if (!isRunning()) { - log.debug("Event source not yet started. Will not run."); - return; + try { + if (!isRunning()) { + log.debug("Event source not yet started. Will not run."); + return; + } + getStateAndFillCache(); + healthy.set(true); + } catch (RuntimeException e) { + healthy.set(false); + log.error("Error during polling.", e); } - getStateAndFillCache(); } }, period, @@ -89,7 +98,6 @@ protected synchronized void getStateAndFillCache() { handleResources(values); } - public interface GenericResourceFetcher { Map> fetchResources(); } @@ -99,4 +107,9 @@ public void stop() throws OperatorException { super.stop(); timer.cancel(); } + + @Override + public Status getStatus() { + return healthy.get() ? Status.HEALTHY : Status.UNHEALTHY; + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java index d788f61e4a..1cc98403f8 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java @@ -3,11 +3,14 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.sample.simple.*; +import io.javaoperatorsdk.operator.sample.simple.DuplicateCRController; +import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; +import io.javaoperatorsdk.operator.sample.simple.TestCustomReconcilerOtherV1; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceOtherV1; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -46,19 +49,17 @@ private void checkException( }); final var msg = exception.getMessage(); assertTrue( - msg.contains("Cannot register controller '" + duplicated.getControllerName() + "'") - && msg.contains(registered.getControllerName()) + msg.contains("Cannot register controller '" + duplicated.getName() + "'") + && msg.contains(registered.getName()) && msg.contains(registered.getResourceTypeName())); } private static class TestControllerConfiguration - extends DefaultControllerConfiguration { + extends ResolvedControllerConfiguration { private final Reconciler controller; public TestControllerConfiguration(Reconciler controller, Class crClass) { - super(null, getControllerName(controller), - CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, - null, null, null, null, null, null); + super(crClass, getControllerName(controller), controller.getClass()); this.controller = controller; } @@ -66,9 +67,5 @@ static String getControllerName( Reconciler controller) { return controller.getClass().getSimpleName() + "Controller"; } - - private String getControllerName() { - return getControllerName(controller); - } } } 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 f9f75d6796..98c071e942 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java @@ -6,8 +6,12 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +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; @@ -19,10 +23,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class ControllerConfigurationOverriderTest { + private final BaseConfigurationService configurationService = new BaseConfigurationService(); @Test void overridingNSShouldPreserveUntouchedDependents() { @@ -107,13 +113,13 @@ private KubernetesDependentResourceConfig extractFirstDependentKubernetesResourc private Object extractDependentKubernetesResourceConfig( io.javaoperatorsdk.operator.api.config.ControllerConfiguration configuration, int index) { - return configuration.getDependentResources().get(index).getDependentResourceConfiguration() - .orElseThrow(); + final var spec = configuration.getDependentResources().get(index); + return DependentResourceConfigurationResolver.configurationFor(spec, configuration); } private io.javaoperatorsdk.operator.api.config.ControllerConfiguration createConfiguration( Reconciler reconciler) { - return new AnnotationControllerConfiguration<>(reconciler); + return configurationService.configFor(reconciler); } @ControllerConfiguration(namespaces = "foo") @@ -297,11 +303,12 @@ void replaceNamedDependentResourceConfigShouldWork() { .filter(dr -> dr.getName().equals(dependentResourceName)) .findFirst().orElseThrow(); assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); - var maybeConfig = dependentSpec.getDependentResourceConfiguration(); - assertTrue(maybeConfig.isPresent()); - assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig); + var maybeConfig = + DependentResourceConfigurationResolver.configurationFor(dependentSpec, configuration); + assertNotNull(maybeConfig); + assertTrue(maybeConfig instanceof KubernetesDependentResourceConfig); - var config = (KubernetesDependentResourceConfig) maybeConfig.orElseThrow(); + var config = (KubernetesDependentResourceConfig) maybeConfig; // check that the DependentResource inherits the controller's configuration if applicable assertEquals(1, config.namespaces().size()); assertNull(config.labelSelector()); @@ -318,8 +325,8 @@ void replaceNamedDependentResourceConfigShouldWork() { dependents = overridden.getDependentResources(); dependentSpec = dependents.stream().filter(dr -> dr.getName().equals(dependentResourceName)) .findFirst().orElseThrow(); - config = (KubernetesDependentResourceConfig) dependentSpec.getDependentResourceConfiguration() - .orElseThrow(); + config = (KubernetesDependentResourceConfig) DependentResourceConfigurationResolver + .configurationFor(dependentSpec, overridden); assertEquals(1, config.namespaces().size()); assertEquals(labelSelector, config.labelSelector()); assertEquals(Set.of(overriddenNS), config.namespaces()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java index 0e3fc5df22..baf680b677 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/MockControllerConfiguration.java @@ -14,6 +14,7 @@ public static ControllerConfiguration forResource( when(configuration.getResourceClass()).thenReturn(resourceType); when(configuration.getNamespaces()).thenReturn(DEFAULT_NAMESPACES_SET); when(configuration.getEffectiveNamespaces()).thenCallRealMethod(); + when(configuration.getName()).thenReturn(resourceType.getSimpleName()); return configuration; } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index 11200f55d9..ec1223377c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -9,10 +9,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.EmptyTestDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; @@ -96,10 +94,6 @@ void getsFirstTypeArgumentFromInterface() { assertThatIllegalArgumentException().isThrownBy( () -> Utils.getFirstTypeArgumentFromInterface(TestKubernetesDependentResource.class, DependentResource.class)); - - assertThat(Utils.getTypeArgumentFromInterfaceByIndex(TestKubernetesDependentResource.class, - AnnotationDependentResourceConfigurator.class, 1)) - .isEqualTo(KubernetesDependentResourceConfig.class); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java new file mode 100644 index 0000000000..3187b32645 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolverTest.java @@ -0,0 +1,228 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Service; +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +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.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentConverter; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; + +import static io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolverTest.CustomAnnotationReconciler.DR_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DependentResourceConfigurationResolverTest { + + // subclass to expose configFor method to this test class + private final static class TestConfigurationService extends BaseConfigurationService { + + @Override + protected

io.javaoperatorsdk.operator.api.config.ControllerConfiguration

configFor( + Reconciler

reconciler) { + return super.configFor(reconciler); + } + } + + private final TestConfigurationService configurationService = new TestConfigurationService(); + + private

io.javaoperatorsdk.operator.api.config.ControllerConfiguration

configFor( + Reconciler

reconciler) { + // ensure that a new configuration is created each time + return configurationService.configFor(reconciler); + } + + @Test + void controllerConfigurationProvidedShouldBeReturnedIfAvailable() { + final var cfg = configFor(new CustomAnnotationReconciler()); + final var customConfig = DependentResourceConfigurationResolver + .extractConfigurationFromConfigured(CustomAnnotatedDep.class, cfg); + assertTrue(customConfig instanceof CustomConfig); + assertEquals(CustomAnnotatedDep.PROVIDED_VALUE, ((CustomConfig) customConfig).getValue()); + final var newConfig = new CustomConfig(72); + final var overridden = ControllerConfigurationOverrider.override(cfg) + .replacingNamedDependentResourceConfig(DR_NAME, newConfig) + .build(); + final var spec = cfg.getDependentResources().stream() + .filter(s -> DR_NAME.equals(s.getName())) + .findFirst() + .orElseThrow(); + assertEquals(newConfig, + DependentResourceConfigurationResolver.configurationFor(spec, overridden)); + } + + @Test + void getConverterShouldWork() { + final var cfg = configFor(new CustomAnnotationReconciler()); + var converter = DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class); + assertNull(converter); + assertNull(DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class)); + + // extracting configuration should trigger converter creation + DependentResourceConfigurationResolver.extractConfigurationFromConfigured( + CustomAnnotatedDep.class, cfg); + converter = DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class); + assertNotNull(converter); + assertEquals(CustomConfigConverter.class, converter.getClass()); + + converter = DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class); + assertNull(converter); + DependentResourceConfigurationResolver.extractConfigurationFromConfigured( + ChildCustomAnnotatedDep.class, cfg); + converter = DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class); + assertNotNull(converter); + assertEquals(CustomConfigConverter.class, converter.getClass()); + assertEquals(DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class), + converter); + } + + @SuppressWarnings("rawtypes") + @Test + void registerConverterShouldWork() { + final var cfg = configFor(new CustomAnnotationReconciler()); + var converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class); + assertNull(converter); + DependentResourceConfigurationResolver.extractConfigurationFromConfigured(ConfigMapDep.class, + cfg); + converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class); + assertTrue(converter instanceof KubernetesDependentConverter); + final var overriddenConverter = new ConfigurationConverter() { + @Override + public Object configFrom(Annotation configAnnotation, + io.javaoperatorsdk.operator.api.config.ControllerConfiguration parentConfiguration, + Class originatingClass) { + return null; + } + }; + DependentResourceConfigurationResolver.registerConverter(KubernetesDependentResource.class, + overriddenConverter); + + // already resolved converters are kept unchanged + converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class); + assertTrue(converter instanceof KubernetesDependentConverter); + + // but new converters should use the overridden version + DependentResourceConfigurationResolver.extractConfigurationFromConfigured(ServiceDep.class, + cfg); + converter = DependentResourceConfigurationResolver.getConverter(ServiceDep.class); + assertEquals(overriddenConverter, converter); + } + + @ControllerConfiguration(dependents = { + @Dependent(type = CustomAnnotatedDep.class, name = DR_NAME), + @Dependent(type = ChildCustomAnnotatedDep.class), + @Dependent(type = ConfigMapDep.class), + @Dependent(type = ServiceDep.class) + }) + static class CustomAnnotationReconciler implements Reconciler { + + public static final String DR_NAME = "first"; + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return null; + } + } + + private static class ConfigMapDep extends KubernetesDependentResource { + + public ConfigMapDep() { + super(ConfigMap.class); + } + } + + private static class ServiceDep extends KubernetesDependentResource { + + public ServiceDep() { + super(Service.class); + } + } + + @CustomAnnotation(value = CustomAnnotatedDep.PROVIDED_VALUE) + @Configured(by = CustomAnnotation.class, with = CustomConfig.class, + converter = CustomConfigConverter.class) + private static class CustomAnnotatedDep implements DependentResource, + DependentResourceConfigurator { + + public static final int PROVIDED_VALUE = 42; + private CustomConfig config; + + @Override + public ReconcileResult reconcile(ConfigMap primary, Context context) { + return null; + } + + @Override + public Class resourceType() { + return ConfigMap.class; + } + + @Override + public void configureWith(CustomConfig config) { + this.config = config; + } + + @Override + public Optional configuration() { + return Optional.ofNullable(config); + } + } + + private static class ChildCustomAnnotatedDep extends CustomAnnotatedDep { + + } + + @Retention(RetentionPolicy.RUNTIME) + private @interface CustomAnnotation { + + int value(); + } + + private static class CustomConfig { + + private final int value; + + private CustomConfig(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private static class CustomConfigConverter + implements ConfigurationConverter { + + static final int CONVERTER_PROVIDED_DEFAULT = 7; + + @Override + public CustomConfig configFrom(CustomAnnotation configAnnotation, + io.javaoperatorsdk.operator.api.config.ControllerConfiguration parentConfiguration, + Class originatingClass) { + if (configAnnotation == null) { + return new CustomConfig(CONVERTER_PROVIDED_DEFAULT); + } else { + return new CustomConfig(configAnnotation.value()); + } + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java index f4ef13d122..0f6fea2449 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java @@ -8,13 +8,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import static io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowTestUtils.createDRS; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; class ManagedWorkflowSupportTest { @@ -140,18 +138,12 @@ void createsWorkflow() { createDRS(NAME_3, NAME_1), createDRS(NAME_4, NAME_3, NAME_2)); - final var client = mock(KubernetesClient.class); - var workflow = managedWorkflowSupport.createWorkflow(specs); - workflow.resolve(client, specs); + var workflow = managedWorkflowSupport.createAsDefault(specs); - assertThat(workflow.nodes().values()).map(DependentResourceNode::getName) + assertThat(workflow.nodeNames()) .containsExactlyInAnyOrder(NAME_1, NAME_2, NAME_3, NAME_4); - assertThat(workflow.getTopLevelDependentResources()) - .map(DependentResourceNode::getName) - .containsExactly(NAME_1); - assertThat(workflow.getBottomLevelResource()) - .map(DependentResourceNode::getName) - .containsExactly(NAME_4); + assertThat(workflow.getTopLevelResources()).containsExactly(NAME_1); + assertThat(workflow.getBottomLevelResources()).containsExactly(NAME_4); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java index 96a55aa85f..68e3068de2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; @@ -17,44 +16,54 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@SuppressWarnings({"rawtypes", "unchecked"}) +@SuppressWarnings({"rawtypes"}) class ManagedWorkflowTest { public static final String NAME = "name"; @Test void checksIfWorkflowEmpty() { - assertThat(managedWorkflow().isEmptyWorkflow()).isTrue(); - assertThat(managedWorkflow(createDRS(NAME)).isEmptyWorkflow()).isFalse(); + assertThat(managedWorkflow().isEmpty()).isTrue(); + assertThat(managedWorkflow(createDRS(NAME)).isEmpty()).isFalse(); } @Test void isNotCleanerIfNoDeleter() { - assertThat(managedWorkflow(createDRS(NAME)).isCleaner()).isFalse(); + assertThat(managedWorkflow(createDRS(NAME)).hasCleaner()).isFalse(); } @Test void isNotCleanerIfGarbageCollected() { assertThat(managedWorkflow(createDRSWithTraits(NAME, GarbageCollected.class)) - .isCleaner()).isFalse(); + .hasCleaner()).isFalse(); + } + + @Test + void isCleanerShouldWork() { + assertThat(managedWorkflow( + createDRSWithTraits(NAME, GarbageCollected.class), + createDRSWithTraits("foo", Deleter.class)) + .hasCleaner()).isTrue(); + + assertThat(managedWorkflow( + createDRSWithTraits("foo", Deleter.class), + createDRSWithTraits(NAME, GarbageCollected.class)) + .hasCleaner()).isTrue(); } @Test void isCleanerIfHasDeleter() { var spec = createDRSWithTraits(NAME, Deleter.class); - assertThat(managedWorkflow(spec).isCleaner()).isTrue(); + assertThat(managedWorkflow(spec).hasCleaner()).isTrue(); } ManagedWorkflow managedWorkflow(DependentResourceSpec... specs) { final var configuration = mock(ControllerConfiguration.class); final var specList = List.of(specs); - KubernetesClient kubernetesClientMock = mock(KubernetesClient.class); - when(configuration.getDependentResources()).thenReturn(specList); return ConfigurationServiceProvider.instance().getWorkflowFactory() - .workflowFor(configuration) - .resolve(kubernetesClientMock, specList); + .workflowFor(configuration); } } 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 52c6840b1c..25c0ad139b 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 @@ -9,6 +9,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; import io.javaoperatorsdk.operator.processing.dependent.EmptyTestDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -19,7 +20,7 @@ public class ManagedWorkflowTestUtils { @SuppressWarnings("unchecked") public static DependentResourceSpec createDRS(String name, String... dependOns) { - return new DependentResourceSpec(new EmptyTestDependentResource(), name, Set.of(dependOns), + return new DependentResourceSpec(EmptyTestDependentResource.class, name, Set.of(dependOns), null, null, null, null); } @@ -31,13 +32,12 @@ public static DependentResourceSpec createDRSWithTraits(String name, Class toMock = DependentResource.class; final var garbageCollected = dependentResourceTraits != null && Arrays.asList(dependentResourceTraits).contains(GarbageCollected.class); + if (garbageCollected) { + toMock = KubernetesDependentResource.class; + } final var dr = mock(toMock, withSettings().extraInterfaces(dependentResourceTraits)); - // it would be better to call the real method here but it doesn't work because - // KubernetesDependentResource checks for GarbageCollected trait when instantiated which doesn't - // happen when using mocks - when(dr.isDeletable()).thenReturn(!garbageCollected); - when(spy.getDependentResource()).thenReturn(dr); + when(spy.getDependentResourceClass()).thenReturn(dr.getClass()); return spy; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java index 7da98f66b2..4e6cc5c329 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java @@ -5,11 +5,17 @@ import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; @SuppressWarnings("rawtypes") class WorkflowTest { @@ -28,7 +34,7 @@ void calculatesTopLevelResources() { Set topResources = workflow.getTopLevelDependentResources().stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toSet()); assertThat(topResources).containsExactlyInAnyOrder(dr1, independentDR); @@ -48,10 +54,29 @@ void calculatesBottomLevelResources() { Set bottomResources = workflow.getBottomLevelResource().stream() - .map(workflow::getDependentResourceFor) + .map(DependentResourceNode::getDependentResource) .collect(Collectors.toSet()); assertThat(bottomResources).containsExactlyInAnyOrder(dr2, independentDR); } + + @Test + void isDeletableShouldWork() { + var dr = mock(DependentResource.class); + assertFalse(DefaultWorkflow.isDeletable(dr.getClass())); + + dr = mock(DependentResource.class, withSettings().extraInterfaces(Deleter.class)); + assertTrue(DefaultWorkflow.isDeletable(dr.getClass())); + + dr = mock(KubernetesDependentResource.class); + assertFalse(DefaultWorkflow.isDeletable(dr.getClass())); + + dr = mock(KubernetesDependentResource.class, withSettings().extraInterfaces(Deleter.class)); + assertTrue(DefaultWorkflow.isDeletable(dr.getClass())); + + dr = mock(KubernetesDependentResource.class, withSettings().extraInterfaces(Deleter.class, + GarbageCollected.class)); + assertFalse(DefaultWorkflow.isDeletable(dr.getClass())); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index 23d5961015..583e7a7289 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -120,7 +120,9 @@ void ifExecutionInProgressWaitsUntilItsFinished() { void schedulesAnEventRetryOnException() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null); + executionScope.setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); @@ -254,7 +256,7 @@ void cancelScheduleOnceEventsOnSuccessfulExecution() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), + eventProcessor.eventProcessingFinished(new ExecutionScope(null).setResource(cr), PostExecutionControl.defaultDispatch()); verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); @@ -277,13 +279,14 @@ void startProcessedMarkedEventReceivedBefore() { eventProcessor.start(); verify(reconciliationDispatcherMock, timeout(100).times(1)).handleExecution(any()); - verify(metricsMock, times(1)).reconcileCustomResource(any(), isNull(), any()); + verify(metricsMock, times(1)).reconcileCustomResource(any(HasMetadata.class), isNull(), any()); } @Test void updatesEventSourceHandlerIfResourceUpdated() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceUpdated(customResource); @@ -297,7 +300,8 @@ void updatesEventSourceHandlerIfResourceUpdated() { @Test void notUpdatesEventSourceHandlerIfResourceUpdated() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceStatusPatched(customResource); @@ -311,7 +315,8 @@ void notUpdatesEventSourceHandlerIfResourceUpdated() { void notReschedulesAfterTheFinalizerRemoveProcessed() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); @@ -324,7 +329,8 @@ void notReschedulesAfterTheFinalizerRemoveProcessed() { void skipEventProcessingIfFinalizerRemoveProcessed() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); @@ -341,7 +347,8 @@ void skipEventProcessingIfFinalizerRemoveProcessed() { void newResourceAfterMissedDeleteEvent() { TestCustomResource customResource = testCustomResource(); markForDeletion(customResource); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.customResourceFinalizerRemoved(customResource); var newResource = testCustomResource(); @@ -377,7 +384,8 @@ void rateLimitsReconciliationSubmission() { @Test void schedulesRetryForMarReconciliationInterval() { TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(customResource, null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(customResource); PostExecutionControl postExecutionControl = PostExecutionControl.defaultDispatch(); @@ -398,7 +406,8 @@ void schedulesRetryForMarReconciliationIntervalIfRetryExhausted() { eventSourceManagerMock, metricsMock)); eventProcessorWithRetry.start(); - ExecutionScope executionScope = new ExecutionScope(testCustomResource(), null); + ExecutionScope executionScope = + new ExecutionScope(null).setResource(testCustomResource()); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException()); when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 892fffcdbb..ef7a27677b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -22,10 +22,7 @@ import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.Cloner; -import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.MockControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.*; import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -137,8 +134,9 @@ void addFinalizerOnNewResource() { verify(reconciler, never()) .reconcile(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) - .updateResource( - argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); + .serverSideApplyLockResource( + argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER)), + any()); assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)).isTrue(); } @@ -218,7 +216,8 @@ void removesDefaultFinalizerOnDeleteIfSet() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(postExecControl.isFinalizerRemoved()).isTrue(); - verify(customResourceFacade, times(1)).updateResource(testCustomResource); + verify(customResourceFacade, times(1)).serverSideApplyLockResource(testCustomResource, + testCustomResource); } @Test @@ -227,7 +226,7 @@ void retriesFinalizerRemovalWithFreshResource() { markForDeletion(testCustomResource); var resourceWithFinalizer = TestUtils.testCustomResource(); resourceWithFinalizer.addFinalizer(DEFAULT_FINALIZER); - when(customResourceFacade.updateResource(testCustomResource)) + when(customResourceFacade.serverSideApplyLockResource(testCustomResource, testCustomResource)) .thenThrow(new KubernetesClientException(null, 409, null)) .thenReturn(testCustomResource); when(customResourceFacade.getResource(any(), any())).thenReturn(resourceWithFinalizer); @@ -236,7 +235,7 @@ void retriesFinalizerRemovalWithFreshResource() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(postExecControl.isFinalizerRemoved()).isTrue(); - verify(customResourceFacade, times(2)).updateResource(any()); + verify(customResourceFacade, times(2)).serverSideApplyLockResource(any(), any()); verify(customResourceFacade, times(1)).getResource(any(), any()); } @@ -244,7 +243,7 @@ void retriesFinalizerRemovalWithFreshResource() { void throwsExceptionIfFinalizerRemovalRetryExceeded() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(customResourceFacade.updateResource(any())) + when(customResourceFacade.serverSideApplyLockResource(any(), any())) .thenThrow(new KubernetesClientException(null, 409, null)); when(customResourceFacade.getResource(any(), any())) .thenAnswer((Answer) invocationOnMock -> createResourceWithFinalizer()); @@ -256,7 +255,9 @@ void throwsExceptionIfFinalizerRemovalRetryExceeded() { assertThat(postExecControl.getRuntimeException()).isPresent(); assertThat(postExecControl.getRuntimeException().get()) .isInstanceOf(OperatorException.class); - verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY)).updateResource(any()); + verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY)).serverSideApplyLockResource( + any(), + any()); verify(customResourceFacade, times(MAX_FINALIZER_REMOVAL_RETRY - 1)).getResource(any(), any()); } @@ -265,7 +266,7 @@ void throwsExceptionIfFinalizerRemovalRetryExceeded() { void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(customResourceFacade.updateResource(any())) + when(customResourceFacade.serverSideApplyLockResource(any(), any())) .thenThrow(new KubernetesClientException(null, 400, null)); var res = @@ -273,7 +274,7 @@ void throwsExceptionIfFinalizerRemovalClientExceptionIsNotConflict() { assertThat(res.getRuntimeException()).isPresent(); assertThat(res.getRuntimeException().get()).isInstanceOf(KubernetesClientException.class); - verify(customResourceFacade, times(1)).updateResource(any()); + verify(customResourceFacade, times(1)).serverSideApplyLockResource(any(), any()); verify(customResourceFacade, never()).getResource(any(), any()); } @@ -337,13 +338,14 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); - when(customResourceFacade.updateResource(any())).thenReturn(testCustomResource); + when(customResourceFacade.serverSideApplyLockResource(any(), any())) + .thenReturn(testCustomResource); var postExecControl = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); - verify(customResourceFacade, times(1)).updateResource(any()); + verify(customResourceFacade, times(1)).serverSideApplyLockResource(any(), any()); assertThat(postExecControl.updateIsStatusPatch()).isFalse(); assertThat(postExecControl.getUpdatedCustomResource()).isPresent(); } @@ -374,7 +376,6 @@ void propagatesRetryInfoToContextIfFinalizerSet() { reconciliationDispatcher.handleExecution( new ExecutionScope( - testCustomResource, new RetryInfo() { @Override public int getAttemptCount() { @@ -385,7 +386,7 @@ public int getAttemptCount() { public boolean isLastAttempt() { return true; } - })); + }).setResource(testCustomResource)); ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(Context.class); @@ -504,7 +505,6 @@ void callErrorStatusHandlerIfImplemented() { reconciliationDispatcher.handleExecution( new ExecutionScope( - testCustomResource, new RetryInfo() { @Override public int getAttemptCount() { @@ -515,7 +515,7 @@ public int getAttemptCount() { public boolean isLastAttempt() { return true; } - })); + }).setResource(testCustomResource)); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), @@ -535,8 +535,7 @@ void callErrorStatusHandlerEvenOnFirstError() { }; var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); @@ -555,8 +554,7 @@ void errorHandlerCanInstructNoRetryWithUpdate() { }; var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); @@ -576,8 +574,7 @@ void errorHandlerCanInstructNoRetryNoUpdate() { }; var postExecControl = reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); @@ -596,8 +593,7 @@ void errorStatusHandlerCanPatchResource() { reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(customResourceFacade, times(1)).patchStatus(eq(testCustomResource), any()); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), @@ -622,8 +618,7 @@ void ifRetryLimitedToZeroMaxAttemptsErrorHandlerGetsCorrectLastAttempt() { reconciler.errorHandler = mockErrorHandler; reconciliationDispatcher.handleExecution( - new ExecutionScope( - testCustomResource, null)); + new ExecutionScope(null).setResource(testCustomResource)); verify(mockErrorHandler, times(1)).updateErrorStatus(any(), ArgumentMatchers.argThat((ArgumentMatcher>) context -> { @@ -646,6 +641,24 @@ void canSkipSchedulingMaxDelayIf() { assertThat(control.getReScheduleDelay()).isNotPresent(); } + @Test + void retriesAddingFinalizer() { + removeFinalizers(testCustomResource); + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); + when(customResourceFacade.serverSideApplyLockResource(any(), any())) + .thenThrow(new KubernetesClientException(null, 409, null)) + .thenReturn(testCustomResource); + when(customResourceFacade.getResource(any(), any())) + .then((Answer) invocationOnMock -> { + testCustomResource.getFinalizers().clear(); + return testCustomResource; + }); + + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + + verify(customResourceFacade, times(2)).serverSideApplyLockResource(any(), any()); + } + private ObservedGenCustomResource createObservedGenCustomResource() { ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); observedGenCustomResource.setMetadata(new ObjectMeta()); @@ -667,7 +680,7 @@ private void removeFinalizers(CustomResource customResource) { } public ExecutionScope executionScopeWithCREvent(T resource) { - return new ExecutionScope<>(resource, null); + return (ExecutionScope) new ExecutionScope<>(null).setResource(resource); } private class TestReconciler diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java index 7cc5a20781..26994cee1a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java @@ -8,9 +8,10 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.MockKubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; @@ -25,7 +26,8 @@ import static org.mockito.Mockito.verify; class ResourceEventFilterTest { - public static final String FINALIZER = "finalizer"; + public static final String FINALIZER = + ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); private EventHandler eventHandler; @@ -129,23 +131,21 @@ public TestControllerConfig(String finalizer, boolean generationAware, } private static class ControllerConfig extends - DefaultControllerConfiguration { + ResolvedControllerConfiguration { public ControllerConfig(String finalizer, boolean generationAware, ResourceEventFilter eventFilter, Class customResourceClass) { - super( - null, + super(customResourceClass, + "test", + generationAware, null, null, - finalizer, - generationAware, null, null, null, - eventFilter, - customResourceClass, null, - null, null, null, null, null); + null, null, null, finalizer, null, null); + setEventFilter(eventFilter); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 00980743e0..5a171195e1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -7,8 +7,9 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.MockKubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; @@ -19,12 +20,17 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; class ControllerResourceEventSourceTest extends AbstractEventSourceTestBase, EventHandler> { - public static final String FINALIZER = "finalizer"; + public static final String FINALIZER = + ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); private final TestController testController = new TestController(true); @@ -171,24 +177,27 @@ public boolean useFinalizer() { } private static class TestConfiguration extends - DefaultControllerConfiguration { + ResolvedControllerConfiguration { public TestConfiguration(boolean generationAware, OnAddFilter onAddFilter, OnUpdateFilter onUpdateFilter, GenericFilter genericFilter) { super( + TestCustomResource.class, + "test", + generationAware, null, null, null, - FINALIZER, - generationAware, null, + onAddFilter, + onUpdateFilter, + genericFilter, null, null, null, - TestCustomResource.class, - null, - onAddFilter, onUpdateFilter, genericFilter, null, null); + FINALIZER, + null, null); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java index 2af66a4abe..2e77a557ea 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java @@ -1,11 +1,12 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.processing.event.ResourceID; import static org.assertj.core.api.Assertions.assertThat; @@ -18,7 +19,7 @@ class TemporaryResourceCacheTest { public static final String RESOURCE_VERSION = "1"; private InformerEventSource informerEventSource = mock(InformerEventSource.class); private TemporaryResourceCache temporaryResourceCache = - new TemporaryResourceCache<>(informerEventSource); + new TemporaryResourceCache<>(informerEventSource, null); @Test @@ -79,6 +80,29 @@ void removesResourceFromCache() { .isNotPresent(); } + @Test + void objectIsTransformedBeforePutIntoCache() { + temporaryResourceCache = + new TemporaryResourceCache<>(informerEventSource, r -> { + r.getMetadata().setLabels(null); + return r; + }); + + temporaryResourceCache.putAddedResource(testResource()); + assertLabelsIsEmpty(temporaryResourceCache); + + temporaryResourceCache.unconditionallyCacheResource(testResource()); + assertLabelsIsEmpty(temporaryResourceCache); + + temporaryResourceCache.unconditionallyCacheResource(testResource()); + assertLabelsIsEmpty(temporaryResourceCache); + } + + private void assertLabelsIsEmpty(TemporaryResourceCache temporaryResourceCache) { + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource())) + .orElseThrow().getMetadata().getLabels()).isNull(); + } + private ConfigMap propagateTestResourceToCache() { var testResource = testResource(); when(informerEventSource.get(any())).thenReturn(Optional.empty()); @@ -90,7 +114,9 @@ private ConfigMap propagateTestResourceToCache() { ConfigMap testResource() { ConfigMap configMap = new ConfigMap(); - configMap.setMetadata(new ObjectMeta()); + configMap.setMetadata(new ObjectMetaBuilder() + .withLabels(Map.of("k", "v")) + .build()); configMap.getMetadata().setName("test"); configMap.getMetadata().setNamespace("default"); configMap.getMetadata().setResourceVersion(RESOURCE_VERSION); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStoreTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStoreTest.java new file mode 100644 index 0000000000..3bebc79094 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TransformingItemStoreTest.java @@ -0,0 +1,59 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; + +import static io.fabric8.kubernetes.client.informers.cache.Cache.metaNamespaceKeyFunc; +import static org.assertj.core.api.Assertions.assertThat; + +class TransformingItemStoreTest { + + @Test + void cachedObjectTransformed() { + TransformingItemStore transformingItemStore = new TransformingItemStore<>(r -> { + r.getMetadata().setLabels(null); + return r; + }); + + var cm = configMap(); + cm.getMetadata().setLabels(Map.of("k", "v")); + transformingItemStore.put(metaNamespaceKeyFunc(cm), cm); + + assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getLabels()) + .isNull(); + } + + @Test + void preservesSelectedAttributes() { + TransformingItemStore transformingItemStore = new TransformingItemStore<>(r -> { + r.getMetadata().setName(null); + r.getMetadata().setNamespace(null); + r.getMetadata().setResourceVersion(null); + return r; + }); + var cm = configMap(); + transformingItemStore.put(metaNamespaceKeyFunc(cm), cm); + + assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getName()) + .isNotNull(); + assertThat(transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getNamespace()) + .isNotNull(); + assertThat( + transformingItemStore.get(metaNamespaceKeyFunc(cm)).getMetadata().getResourceVersion()) + .isNotNull(); + } + + ConfigMap configMap() { + var cm = new ConfigMap(); + cm.setMetadata(new ObjectMetaBuilder() + .withName("test1") + .withNamespace("default").withResourceVersion("1") + .build()); + return cm; + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java index 0a777f0cbd..605922f06b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.polling; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -8,12 +9,15 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.health.Status; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.*; class PollingEventSourceTest @@ -21,11 +25,12 @@ class PollingEventSourceTest AbstractEventSourceTestBase, EventHandler> { public static final int DEFAULT_WAIT_PERIOD = 100; + public static final long POLL_PERIOD = 30L; private PollingEventSource.GenericResourceFetcher resourceFetcher = mock(PollingEventSource.GenericResourceFetcher.class); private final PollingEventSource pollingEventSource = - new PollingEventSource<>(resourceFetcher, 30L, SampleExternalResource.class, + new PollingEventSource<>(resourceFetcher, POLL_PERIOD, SampleExternalResource.class, (SampleExternalResource er) -> er.getName() + "#" + er.getValue()); @BeforeEach @@ -73,6 +78,26 @@ void propagatesEventOnNewResourceForPrimary() throws InterruptedException { verify(eventHandler, times(2)).handleEvent(any()); } + @Test + void updatesHealthIndicatorBasedOnExceptionsInFetcher() throws InterruptedException { + when(resourceFetcher.fetchResources()) + .thenReturn(testResponseWithOneValue()); + pollingEventSource.start(); + assertThat(pollingEventSource.getStatus()).isEqualTo(Status.HEALTHY); + + when(resourceFetcher.fetchResources()) + // 2x - to make sure to catch the health indicator change + .thenThrow(new RuntimeException("test exception")) + .thenThrow(new RuntimeException("test exception")) + .thenReturn(testResponseWithOneValue()); + + await().pollInterval(Duration.ofMillis(POLL_PERIOD)).untilAsserted( + () -> assertThat(pollingEventSource.getStatus()).isEqualTo(Status.UNHEALTHY)); + + await() + .untilAsserted(() -> assertThat(pollingEventSource.getStatus()).isEqualTo(Status.HEALTHY)); + } + private Map> testResponseWithTwoValueForSameId() { Map> res = new HashMap<>(); res.put(primaryID1(), Set.of(testResource1(), testResource2())); diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 891aab5c96..94ac00cacb 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 27efa90b55..7b2c68639f 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java deleted file mode 100644 index 47eee2a249..0000000000 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.javaoperatorsdk.operator.config.runtime; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; - -public class AnnotationControllerConfiguration - extends io.javaoperatorsdk.operator.api.config.AnnotationControllerConfiguration { - - public AnnotationControllerConfiguration(Reconciler reconciler) { - super(reconciler); - } - - @Override - public Class getResourceClass() { - return RuntimeControllerMetadata.getResourceClass(reconciler); - } -} diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java index 34e0b3f1d7..49f0ed2b67 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationService.java @@ -4,6 +4,7 @@ import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; public class DefaultConfigurationService extends BaseConfigurationService { @@ -46,6 +47,8 @@ public void setCreateIfNeeded(boolean createIfNeeded) { @Override protected ControllerConfiguration configFor(Reconciler reconciler) { - return new AnnotationControllerConfiguration<>(reconciler); + final var other = super.configFor(reconciler); + return new ResolvedControllerConfiguration<>( + RuntimeControllerMetadata.getResourceClass(reconciler), other); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java new file mode 100644 index 0000000000..9bf57ab372 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CachePruneIT.java @@ -0,0 +1,77 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneCustomResource; +import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneReconciler; +import io.javaoperatorsdk.operator.sample.cacheprune.CachePruneSpec; + +import static io.javaoperatorsdk.operator.sample.cacheprune.CachePruneReconciler.DATA_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class CachePruneIT { + + public static final String DEFAULT_DATA = "default_data"; + public static final String TEST_RESOURCE_NAME = "test1"; + public static final String UPDATED_DATA = "updated_data"; + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler(new CachePruneReconciler()).build(); + + @Test + void pruningRelatedBehavior() { + var res = operator.create(testResource()); + await().untilAsserted(() -> { + assertState(DEFAULT_DATA); + }); + + res.getSpec().setData(UPDATED_DATA); + var updated = operator.replace(res); + + await().untilAsserted(() -> { + assertState(UPDATED_DATA); + }); + + operator.delete(updated); + + await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> { + var actual = operator.get(CachePruneCustomResource.class, TEST_RESOURCE_NAME); + var configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(configMap).isNull(); + assertThat(actual).isNull(); + }); + } + + void assertState(String expectedData) { + var actual = operator.get(CachePruneCustomResource.class, TEST_RESOURCE_NAME); + assertThat(actual.getMetadata()).isNotNull(); + assertThat(actual.getMetadata().getFinalizers()).isNotEmpty(); + assertThat(actual.getStatus().getCreated()).isTrue(); + assertThat(actual.getMetadata().getLabels()).isNotEmpty(); + var configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(configMap.getData()).containsEntry(DATA_KEY, expectedData); + assertThat(configMap.getMetadata().getLabels()).isNotEmpty(); + } + + CachePruneCustomResource testResource() { + var res = new CachePruneCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .withLabels(Map.of("sampleLabel", "val")) + .build()); + res.setSpec(new CachePruneSpec()); + res.getSpec().setData(DEFAULT_DATA); + return res; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java index 56eda890a1..c7d3b04c4a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ChangeNamespaceIT.java @@ -4,6 +4,8 @@ import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -12,6 +14,7 @@ import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; import io.javaoperatorsdk.operator.sample.changenamespace.ChangeNamespaceTestCustomResource; import io.javaoperatorsdk.operator.sample.changenamespace.ChangeNamespaceTestReconciler; @@ -25,63 +28,98 @@ class ChangeNamespaceIT { public static final String TEST_RESOURCE_NAME_2 = "test2"; public static final String TEST_RESOURCE_NAME_3 = "test3"; public static final String ADDITIONAL_TEST_NAMESPACE = "additional-test-namespace"; + @RegisterExtension LocallyRunOperatorExtension operator = LocallyRunOperatorExtension.builder().withReconciler(new ChangeNamespaceTestReconciler()) .build(); + @BeforeEach + void setup() { + client().namespaces().resource(additionalTestNamespace()).create(); + } + + @AfterEach + void cleanup() { + client().namespaces().resource(additionalTestNamespace()).delete(); + } + @SuppressWarnings("rawtypes") @Test void addNewAndRemoveOldNamespaceTest() { - try { - var reconciler = operator.getReconcilerOfType(ChangeNamespaceTestReconciler.class); - var defaultNamespaceResource = operator.create(customResource(TEST_RESOURCE_NAME_1)); + var reconciler = operator.getReconcilerOfType(ChangeNamespaceTestReconciler.class); + var defaultNamespaceResource = operator.create(customResource(TEST_RESOURCE_NAME_1)); + + assertReconciled(reconciler, defaultNamespaceResource); + var resourceInAdditionalTestNamespace = createResourceInAdditionalNamespace(); + + assertNotReconciled(reconciler, resourceInAdditionalTestNamespace); + // adding additional namespace + RegisteredController registeredController = + operator.getRegisteredControllerForReconcile(ChangeNamespaceTestReconciler.class); + registeredController + .changeNamespaces(Set.of(operator.getNamespace(), ADDITIONAL_TEST_NAMESPACE)); + + assertReconciled(reconciler, resourceInAdditionalTestNamespace); - await().pollDelay(Duration.ofMillis(100)).untilAsserted(() -> assertThat( - reconciler.numberOfResourceReconciliations(defaultNamespaceResource)).isEqualTo(2)); + // removing a namespace + registeredController.changeNamespaces(Set.of(ADDITIONAL_TEST_NAMESPACE)); - client().namespaces().create(additionalTestNamespace()); - var resourceInAdditionalTestNamespace = createResourceInTestNamespace(); - await().pollDelay(Duration.ofMillis(200)).untilAsserted( - () -> assertThat( - reconciler.numberOfResourceReconciliations(resourceInAdditionalTestNamespace)) - .isZero()); + var newResourceInDefaultNamespace = operator.create(customResource(TEST_RESOURCE_NAME_3)); + assertNotReconciled(reconciler, newResourceInDefaultNamespace); - // adding additional namespace - RegisteredController registeredController = - operator.getRegisteredControllerForReconcile(ChangeNamespaceTestReconciler.class); - registeredController - .changeNamespaces(Set.of(operator.getNamespace(), ADDITIONAL_TEST_NAMESPACE)); + ConfigMap firstMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME_1); + firstMap.setData(Map.of("data", "newdata")); + operator.replace(firstMap); + assertReconciled(reconciler, defaultNamespaceResource); + } + + @Test + void changeToWatchAllNamespaces() { + var reconciler = operator.getReconcilerOfType(ChangeNamespaceTestReconciler.class); + var resourceInAdditionalTestNamespace = createResourceInAdditionalNamespace(); + + assertNotReconciled(reconciler, resourceInAdditionalTestNamespace); - await().untilAsserted( - () -> assertThat( - reconciler.numberOfResourceReconciliations(resourceInAdditionalTestNamespace)) - .isEqualTo(2)); + var registeredController = + operator.getRegisteredControllerForReconcile(ChangeNamespaceTestReconciler.class); - // removing a namespace - registeredController.changeNamespaces(Set.of(ADDITIONAL_TEST_NAMESPACE)); + registeredController + .changeNamespaces(Set.of(Constants.WATCH_ALL_NAMESPACES)); - var newResourceInDefaultNamespace = operator.create(customResource(TEST_RESOURCE_NAME_3)); - await().pollDelay(Duration.ofMillis(200)) - .untilAsserted(() -> assertThat( - reconciler.numberOfResourceReconciliations(newResourceInDefaultNamespace)).isZero()); + assertReconciled(reconciler, resourceInAdditionalTestNamespace); + registeredController.changeNamespaces(Set.of(operator.getNamespace())); - ConfigMap firstMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME_1); - firstMap.setData(Map.of("data", "newdata")); - operator.replace(firstMap); + var defaultNamespaceResource = operator.create(customResource(TEST_RESOURCE_NAME_1)); + var resource2InAdditionalResource = createResourceInAdditionalNamespace(TEST_RESOURCE_NAME_3); + assertReconciled(reconciler, defaultNamespaceResource); + assertNotReconciled(reconciler, resource2InAdditionalResource); + } + + private static void assertReconciled(ChangeNamespaceTestReconciler reconciler, + ChangeNamespaceTestCustomResource resourceInAdditionalTestNamespace) { + await().untilAsserted( + () -> assertThat( + reconciler.numberOfResourceReconciliations(resourceInAdditionalTestNamespace)) + .isEqualTo(2)); + } - await().untilAsserted(() -> assertThat( - reconciler.numberOfResourceReconciliations(defaultNamespaceResource)).isEqualTo(2)); + private static void assertNotReconciled(ChangeNamespaceTestReconciler reconciler, + ChangeNamespaceTestCustomResource resourceInAdditionalTestNamespace) { + await().pollDelay(Duration.ofMillis(200)).untilAsserted( + () -> assertThat( + reconciler.numberOfResourceReconciliations(resourceInAdditionalTestNamespace)) + .isZero()); + } - } finally { - client().namespaces().delete(additionalTestNamespace()); - } + private ChangeNamespaceTestCustomResource createResourceInAdditionalNamespace() { + return createResourceInAdditionalNamespace(TEST_RESOURCE_NAME_2); } - private ChangeNamespaceTestCustomResource createResourceInTestNamespace() { - var res = customResource(TEST_RESOURCE_NAME_2); + private ChangeNamespaceTestCustomResource createResourceInAdditionalNamespace(String name) { + var res = customResource(name); return client().resources(ChangeNamespaceTestCustomResource.class) .inNamespace(ADDITIONAL_TEST_NAMESPACE) .create(res); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java index f28a6b968f..97f5883a08 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerRelatedBehaviorITS.java @@ -14,9 +14,12 @@ import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestCustomResource; import io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestReconciler; +import static io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestReconciler.CONFIG_MAP_DEPENDENT_RESOURCE; +import static io.javaoperatorsdk.operator.sample.informerrelatedbehavior.InformerRelatedBehaviorTestReconciler.INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -73,21 +76,25 @@ void startsUpWhenNoPermissionToCustomResource() { adminClient.resource(testCustomResource()).createOrReplace(); setNoCustomResourceAccess(); - startOperator(false); + var operator = startOperator(false); assertNotReconciled(); + assertRuntimeInfoNoCRPermission(operator); setFullResourcesAccess(); waitForWatchReconnect(); assertReconciled(); + assertThat(operator.getRuntimeInfo().allEventSourcesAreHealthy()).isTrue(); } + @Test void startsUpWhenNoPermissionToSecondaryResource() { adminClient.resource(testCustomResource()).createOrReplace(); setNoConfigMapAccess(); - startOperator(false); + var operator = startOperator(false); assertNotReconciled(); + assertRuntimeInfoForSecondaryPermission(operator); setFullResourcesAccess(); waitForWatchReconnect(); @@ -184,6 +191,40 @@ private void assertReconciled() { }); } + + private void assertRuntimeInfoNoCRPermission(Operator operator) { + assertThat(operator.getRuntimeInfo().allEventSourcesAreHealthy()).isFalse(); + var unhealthyEventSources = + operator.getRuntimeInfo().unhealthyEventSources() + .get(INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER); + assertThat(unhealthyEventSources).isNotEmpty(); + assertThat(unhealthyEventSources.get(ControllerResourceEventSource.class.getSimpleName())) + .isNotNull(); + var informerHealthIndicators = operator.getRuntimeInfo() + .unhealthyInformerWrappingEventSourceHealthIndicator() + .get(INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER); + assertThat(informerHealthIndicators).isNotEmpty(); + assertThat(informerHealthIndicators.get(ControllerResourceEventSource.class.getSimpleName()) + .informerHealthIndicators()) + .hasSize(1); + } + + private void assertRuntimeInfoForSecondaryPermission(Operator operator) { + assertThat(operator.getRuntimeInfo().allEventSourcesAreHealthy()).isFalse(); + var unhealthyEventSources = + operator.getRuntimeInfo().unhealthyEventSources() + .get(INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER); + assertThat(unhealthyEventSources).isNotEmpty(); + assertThat(unhealthyEventSources.get(CONFIG_MAP_DEPENDENT_RESOURCE)).isNotNull(); + var informerHealthIndicators = operator.getRuntimeInfo() + .unhealthyInformerWrappingEventSourceHealthIndicator() + .get(INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER); + assertThat(informerHealthIndicators).isNotEmpty(); + assertThat( + informerHealthIndicators.get(CONFIG_MAP_DEPENDENT_RESOURCE).informerHealthIndicators()) + .hasSize(1); + } + KubernetesClient clientUsingServiceAccount() { KubernetesClient client = new KubernetesClientBuilder() .withConfig(new ConfigBuilder() @@ -211,7 +252,6 @@ Operator startOperator(boolean stopOnInformerErrorDuringStartup, boolean addStop } }); operator.register(reconciler); - operator.installShutdownHook(); operator.start(); return operator; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java similarity index 68% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java index daf1964d13..22af954661 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.config.runtime; +package io.javaoperatorsdk.operator.config; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -14,8 +14,13 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; +import io.javaoperatorsdk.operator.api.config.dependent.Configured; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -24,6 +29,8 @@ 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; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; @@ -43,11 +50,29 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -class AnnotationControllerConfigurationTest { +class BaseConfigurationServiceTest { + + // subclass to expose configFor method to this test class + private final static class TestConfigurationService extends BaseConfigurationService { + + @Override + protected

io.javaoperatorsdk.operator.api.config.ControllerConfiguration

configFor( + Reconciler

reconciler) { + return super.configFor(reconciler); + } + } + + private final TestConfigurationService configurationService = new TestConfigurationService(); + + private

io.javaoperatorsdk.operator.api.config.ControllerConfiguration

configFor( + Reconciler

reconciler) { + // ensure that a new configuration is created each time + return configurationService.configFor(reconciler); + } @Test void defaultValuesShouldBeConsistent() { - final var configuration = new AnnotationControllerConfiguration<>(new SelectorReconciler()); + final var configuration = configFor(new SelectorReconciler()); final var annotated = extractDependentKubernetesResourceConfig(configuration, 1); final var unannotated = extractDependentKubernetesResourceConfig(configuration, 0); @@ -58,19 +83,19 @@ void defaultValuesShouldBeConsistent() { @SuppressWarnings("rawtypes") private KubernetesDependentResourceConfig extractDependentKubernetesResourceConfig( io.javaoperatorsdk.operator.api.config.ControllerConfiguration configuration, int index) { - return (KubernetesDependentResourceConfig) configuration.getDependentResources().get(index) - .getDependentResourceConfiguration() - .orElseThrow(); + final var spec = configuration.getDependentResources().get(index); + return (KubernetesDependentResourceConfig) DependentResourceConfigurationResolver + .configurationFor(spec, configuration); } @Test @SuppressWarnings("rawtypes") void getDependentResources() { - var configuration = new AnnotationControllerConfiguration<>(new NoDepReconciler()); + var configuration = configFor(new NoDepReconciler()); var dependents = configuration.getDependentResources(); assertTrue(dependents.isEmpty()); - configuration = new AnnotationControllerConfiguration<>(new OneDepReconciler()); + configuration = configFor(new OneDepReconciler()); dependents = configuration.getDependentResources(); assertFalse(dependents.isEmpty()); assertEquals(1, dependents.size()); @@ -78,29 +103,31 @@ void getDependentResources() { assertTrue(dependents.stream().anyMatch(d -> d.getName().equals(dependentResourceName))); var dependentSpec = findByName(dependents, dependentResourceName); assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); - var maybeConfig = dependentSpec.getDependentResourceConfiguration(); - assertTrue(maybeConfig.isPresent()); - assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig); - final var config = (KubernetesDependentResourceConfig) maybeConfig.orElseThrow(); + var maybeConfig = + DependentResourceConfigurationResolver.configurationFor(dependentSpec, configuration); + assertNotNull(maybeConfig); + assertTrue(maybeConfig instanceof KubernetesDependentResourceConfig); + final var config = (KubernetesDependentResourceConfig) maybeConfig; // check that the DependentResource inherits the controller's configuration if applicable assertEquals(1, config.namespaces().size()); assertEquals(Set.of(OneDepReconciler.CONFIGURED_NS), config.namespaces()); - configuration = new AnnotationControllerConfiguration<>(new NamedDepReconciler()); + configuration = configFor(new NamedDepReconciler()); dependents = configuration.getDependentResources(); assertFalse(dependents.isEmpty()); assertEquals(1, dependents.size()); dependentSpec = findByName(dependents, NamedDepReconciler.NAME); assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); - maybeConfig = dependentSpec.getDependentResourceConfiguration(); - assertTrue(maybeConfig.isPresent()); - assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig); + maybeConfig = DependentResourceConfigurationResolver.configurationFor(dependentSpec, + configuration); + assertNotNull(maybeConfig); + assertTrue(maybeConfig instanceof KubernetesDependentResourceConfig); } @Test void missingAnnotationThrowsException() { Assertions.assertThrows(OperatorException.class, - () -> new AnnotationControllerConfiguration<>(new MissingAnnotationReconciler())); + () -> configFor(new MissingAnnotationReconciler())); } @SuppressWarnings("rawtypes") @@ -118,13 +145,12 @@ private Optional findByNameOptional( @Test void tryingToAddDuplicatedDependentsWithoutNameShouldFail() { - var configuration = new AnnotationControllerConfiguration<>(new DuplicatedDepReconciler()); - assertThrows(IllegalArgumentException.class, configuration::getDependentResources); + assertThrows(IllegalArgumentException.class, () -> configFor(new DuplicatedDepReconciler())); } @Test void addingDuplicatedDependentsWithNameShouldWork() { - var config = new AnnotationControllerConfiguration<>(new NamedDuplicatedDepReconciler()); + var config = configFor(new NamedDuplicatedDepReconciler()); var dependents = config.getDependentResources(); assertEquals(2, dependents.size()); assertTrue(findByNameOptional(dependents, NamedDuplicatedDepReconciler.NAME).isPresent() @@ -134,13 +160,13 @@ && findByNameOptional(dependents, DependentResource.defaultNameFor(ReadOnlyDepen @Test void maxIntervalCanBeConfigured() { - var config = new AnnotationControllerConfiguration<>(new MaxIntervalReconciler()); + var config = configFor(new MaxIntervalReconciler()); assertEquals(50, config.maxReconciliationInterval().map(Duration::getSeconds).orElseThrow()); } @Test void checkDefaultRateAndRetryConfigurations() { - var config = new AnnotationControllerConfiguration<>(new NoDepReconciler()); + var config = configFor(new NoDepReconciler()); final var retry = assertInstanceOf(GenericRetry.class, config.getRetry()); assertEquals(GradualRetry.DEFAULT_MAX_ATTEMPTS, retry.getMaxAttempts()); assertEquals(GradualRetry.DEFAULT_MULTIPLIER, retry.getIntervalMultiplier()); @@ -154,7 +180,7 @@ void checkDefaultRateAndRetryConfigurations() { @Test void configuringRateAndRetryViaAnnotationsShouldWork() { var config = - new AnnotationControllerConfiguration<>(new ConfigurableRateLimitAndRetryReconciler()); + configFor(new ConfigurableRateLimitAndRetryReconciler()); final var retry = config.getRetry(); final var testRetry = assertInstanceOf(TestRetry.class, retry); assertEquals(12, testRetry.getValue()); @@ -166,7 +192,7 @@ void configuringRateAndRetryViaAnnotationsShouldWork() { @Test void checkingRetryingGraduallyWorks() { - var config = new AnnotationControllerConfiguration<>(new CheckRetryingGraduallyConfiguration()); + var config = configFor(new CheckRetryingGraduallyConfiguration()); final var retry = config.getRetry(); final var genericRetry = assertInstanceOf(GenericRetry.class, retry); assertEquals(CheckRetryingGraduallyConfiguration.INITIAL_INTERVAL, @@ -179,10 +205,24 @@ void checkingRetryingGraduallyWorks() { @Test void controllerConfigurationOnSuperClassShouldWork() { - var config = new AnnotationControllerConfiguration<>(new ControllerConfigurationOnSuperClass()); + var config = configFor(new ControllerConfigurationOnSuperClass()); assertNotNull(config.getName()); } + @Test + void configuringFromCustomAnnotationsShouldWork() { + var config = configFor(new CustomAnnotationReconciler()); + assertEquals(CustomAnnotatedDep.PROVIDED_VALUE, getValue(config, 0)); + assertEquals(CustomConfigConverter.CONVERTER_PROVIDED_DEFAULT, getValue(config, 1)); + } + + private static int getValue( + io.javaoperatorsdk.operator.api.config.ControllerConfiguration configuration, int index) { + return ((CustomConfig) DependentResourceConfigurationResolver + .configurationFor(configuration.getDependentResources().get(index), configuration)) + .getValue(); + } + @ControllerConfiguration( maxReconciliationInterval = @MaxReconciliationInterval(interval = 50, timeUnit = TimeUnit.SECONDS)) @@ -353,4 +393,84 @@ public UpdateControl reconcile(ConfigMap resource, Context return null; } } + + @ControllerConfiguration(dependents = { + @Dependent(type = CustomAnnotatedDep.class), + @Dependent(type = ChildCustomAnnotatedDep.class) + }) + private static class CustomAnnotationReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return null; + } + } + + @CustomAnnotation(value = CustomAnnotatedDep.PROVIDED_VALUE) + @Configured(by = CustomAnnotation.class, with = CustomConfig.class, + converter = CustomConfigConverter.class) + private static class CustomAnnotatedDep implements DependentResource, + DependentResourceConfigurator { + + public static final int PROVIDED_VALUE = 42; + private CustomConfig config; + + @Override + public ReconcileResult reconcile(ConfigMap primary, Context context) { + return null; + } + + @Override + public Class resourceType() { + return ConfigMap.class; + } + + @Override + public void configureWith(CustomConfig config) { + this.config = config; + } + + @Override + public Optional configuration() { + return Optional.ofNullable(config); + } + } + + private static class ChildCustomAnnotatedDep extends CustomAnnotatedDep { + + } + + @Retention(RetentionPolicy.RUNTIME) + private @interface CustomAnnotation { + int value(); + } + + private static class CustomConfig { + private final int value; + + private CustomConfig(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private static class CustomConfigConverter + implements ConfigurationConverter { + static final int CONVERTER_PROVIDED_DEFAULT = 7; + + @Override + public CustomConfig configFrom(CustomAnnotation configAnnotation, + io.javaoperatorsdk.operator.api.config.ControllerConfiguration parentConfiguration, + Class originatingClass) { + if (configAnnotation == null) { + return new CustomConfig(CONVERTER_PROVIDED_DEFAULT); + } else { + return new CustomConfig(configAnnotation.value()); + } + } + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneCustomResource.java new file mode 100644 index 0000000000..60431588fd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +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("cpr") +public class CachePruneCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneReconciler.java new file mode 100644 index 0000000000..236b205c2c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneReconciler.java @@ -0,0 +1,107 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.base.PatchContext; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +@ControllerConfiguration(cachePruneFunction = LabelRemovingPruneFunction.class) +public class CachePruneReconciler + implements Reconciler, + EventSourceInitializer, + Cleaner, KubernetesClientAware { + + public static final String DATA_KEY = "data"; + public static final String FIELD_MANAGER = "controller"; + public static final String SECONDARY_CREATE_FIELD_MANAGER = "creator"; + private KubernetesClient client; + + @Override + public UpdateControl reconcile( + CachePruneCustomResource resource, + Context context) { + var configMap = context.getSecondaryResource(ConfigMap.class); + configMap.ifPresentOrElse(cm -> { + if (!cm.getMetadata().getLabels().isEmpty()) { + throw new AssertionError("Labels should be null"); + } + if (!cm.getData().get(DATA_KEY) + .equals(resource.getSpec().getData())) { + var cloned = ConfigurationServiceProvider.instance().getResourceCloner().clone(cm); + cloned.getData().put(DATA_KEY, resource.getSpec().getData()); + var patchContext = patchContextWithFieldManager(FIELD_MANAGER); + // setting new field manager since we don't control label anymore: + // since not the whole object is present in cache SSA would remove labels if the controller + // is not the manager. + // Note that JSON Merge Patch (or others would also work here, without this "hack". + patchContext.setForce(true); + patchContext.setFieldManager(FIELD_MANAGER); + client.configMaps().resource(cm) + .patch(patchContext, cloned); + } + }, () -> client.configMaps().resource(configMap(resource)) + .patch(patchContextWithFieldManager(SECONDARY_CREATE_FIELD_MANAGER))); + + resource.setStatus(new CachePruneStatus()); + resource.getStatus().setCreated(true); + return UpdateControl.patchStatus(resource); + } + + private PatchContext patchContextWithFieldManager(String fieldManager) { + PatchContext patchContext = new PatchContext(); + // using server side apply + patchContext.setPatchType(PatchType.SERVER_SIDE_APPLY); + patchContext.setFieldManager(fieldManager); + return patchContext; + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + InformerEventSource configMapEventSource = + new InformerEventSource<>(InformerConfiguration.from(ConfigMap.class, context) + .withCachePruneFunction(new LabelRemovingPruneFunction<>()) + .build(), + context); + return EventSourceInitializer.nameEventSources(configMapEventSource); + } + + ConfigMap configMap(CachePruneCustomResource resource) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName(resource.getMetadata().getName()); + configMap.getMetadata().setNamespace(resource.getMetadata().getNamespace()); + configMap.setData(Map.of(DATA_KEY, resource.getSpec().getData())); + HashMap labels = new HashMap<>(); + labels.put("mylabel", "val"); + configMap.getMetadata().setLabels(labels); + configMap.addOwnerReference(resource); + return configMap; + } + + @Override + public KubernetesClient getKubernetesClient() { + return client; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.client = kubernetesClient; + } + + @Override + public DeleteControl cleanup(CachePruneCustomResource resource, + Context context) { + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneSpec.java new file mode 100644 index 0000000000..2d58a70d3a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +public class CachePruneSpec { + + private String data; + + public String getData() { + return data; + } + + public CachePruneSpec setData(String data) { + this.data = data; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneStatus.java new file mode 100644 index 0000000000..a074c0e011 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/CachePruneStatus.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +public class CachePruneStatus { + + private Boolean created; + + public Boolean getCreated() { + return created; + } + + public CachePruneStatus setCreated(Boolean created) { + this.created = created; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/LabelRemovingPruneFunction.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/LabelRemovingPruneFunction.java new file mode 100644 index 0000000000..a495803628 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cacheprune/LabelRemovingPruneFunction.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.sample.cacheprune; + +import java.util.function.UnaryOperator; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class LabelRemovingPruneFunction implements UnaryOperator { + @Override + public R apply(R r) { + r.getMetadata().setLabels(null); + return r; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java index a572572a97..ebc1655c38 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java @@ -5,21 +5,24 @@ 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.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +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.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @ControllerConfiguration( - dependents = @Dependent( - type = BulkDependentResourceExternalWithState.class)) + dependents = @Dependent(type = BulkDependentResourceExternalWithState.class)) public class ExternalStateBulkDependentReconciler implements Reconciler, EventSourceInitializer, TestExecutionInfoProvider { - public static final String ID_KEY = "id"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java index baeff08478..13057d547a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informerrelatedbehavior/InformerRelatedBehaviorTestReconciler.java @@ -10,10 +10,17 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -@ControllerConfiguration(dependents = @Dependent(type = ConfigMapDependentResource.class)) +@ControllerConfiguration( + name = InformerRelatedBehaviorTestReconciler.INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER, + dependents = @Dependent( + name = InformerRelatedBehaviorTestReconciler.CONFIG_MAP_DEPENDENT_RESOURCE, + type = ConfigMapDependentResource.class)) public class InformerRelatedBehaviorTestReconciler implements Reconciler, TestExecutionInfoProvider { + public static final String INFORMER_RELATED_BEHAVIOR_TEST_RECONCILER = + "InformerRelatedBehaviorTestReconciler"; + public static final String CONFIG_MAP_DEPENDENT_RESOURCE = "ConfigMapDependentResource"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private KubernetesClient client; diff --git a/pom.xml b/pom.xml index a9070ebf06..4abd8540c1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom @@ -43,7 +43,7 @@ https://sonarcloud.io 5.9.1 - 6.2.0 + 6.3.0 1.7.36 2.19.0 4.9.0 diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index a0f996983d..82fdd327ac 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-leader-election diff --git a/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java index 6bb171cfd5..262c0a7c70 100644 --- a/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java +++ b/sample-operators/leader-election/src/main/java/io/javaoperatorsdk/operator/sample/LeaderElectionTestOperator.java @@ -27,7 +27,6 @@ public static void main(String[] args) { new Operator(client, c -> c.withLeaderElectionConfiguration(leaderElectionConfiguration)); operator.register(new LeaderElectionTestReconciler(identity)); - operator.installShutdownHook(); operator.start(); } } diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 888a93abb5..4f98b6c1ba 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java index b2fb0aab65..4128dd0ea8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -34,7 +34,6 @@ public static void main(String[] args) throws IOException { configOverrider -> configOverrider.replacingNamedDependentResourceConfig( SchemaDependentResource.NAME, new ResourcePollerConfig(300, MySQLDbConfig.loadFromEnvironmentVars()))); - operator.installShutdownHook(); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); 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 8fb8855e41..d7ee5d368f 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 @@ -13,13 +13,16 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; +import io.javaoperatorsdk.operator.api.config.dependent.Configured; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; import io.javaoperatorsdk.operator.processing.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; import io.javaoperatorsdk.operator.sample.MySQLDbConfig; import io.javaoperatorsdk.operator.sample.MySQLSchema; +import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource.ResourcePollerConfigConverter; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; @@ -30,9 +33,11 @@ @SchemaConfig(pollPeriod = 700, host = "127.0.0.1", port = SchemaDependentResource.LOCAL_PORT, user = "root", password = "password") // NOSONAR: password is only used locally, example only +@Configured(by = SchemaConfig.class, with = ResourcePollerConfig.class, + converter = ResourcePollerConfigConverter.class) public class SchemaDependentResource extends PerResourcePollingDependentResource - implements AnnotationDependentResourceConfigurator, + implements DependentResourceConfigurator, Creator, Deleter { public static final String NAME = "schema"; @@ -56,18 +61,6 @@ public void configureWith(ResourcePollerConfig config) { setPollingPeriod(config.getPollPeriod()); } - @Override - public ResourcePollerConfig configFrom(SchemaConfig annotation, - ControllerConfiguration parentConfiguration) { - if (annotation != null) { - return new ResourcePollerConfig(annotation.pollPeriod(), - new MySQLDbConfig(annotation.host(), "" + annotation.port(), - annotation.user(), annotation.password())); - } - return new ResourcePollerConfig(SchemaConfig.DEFAULT_POLL_PERIOD, - MySQLDbConfig.loadFromEnvironmentVars()); - } - @Override public Schema desired(MySQLSchema primary, Context context) { return new Schema(primary.getMetadata().getName(), primary.getSpec().getEncoding()); @@ -120,4 +113,21 @@ public Set fetchResources(MySQLSchema primaryResource) { throw new RuntimeException("Error while trying read Schema", e); } } + + static class ResourcePollerConfigConverter implements + ConfigurationConverter { + + @Override + public ResourcePollerConfig configFrom(SchemaConfig configAnnotation, + ControllerConfiguration parentConfiguration, + Class originatingClass) { + if (configAnnotation != null) { + return new ResourcePollerConfig(configAnnotation.pollPeriod(), + new MySQLDbConfig(configAnnotation.host(), "" + configAnnotation.port(), + configAnnotation.user(), configAnnotation.password())); + } + return new ResourcePollerConfig(SchemaConfig.DEFAULT_POLL_PERIOD, + MySQLDbConfig.loadFromEnvironmentVars()); + } + } } diff --git a/sample-operators/mysql-schema/src/main/resources/log4j2.xml b/sample-operators/mysql-schema/src/main/resources/log4j2.xml index 5ab4735126..01484221f9 100644 --- a/sample-operators/mysql-schema/src/main/resources/log4j2.xml +++ b/sample-operators/mysql-schema/src/main/resources/log4j2.xml @@ -6,7 +6,7 @@ - + diff --git a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java index 24e257956b..e6aa796656 100644 --- a/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -53,7 +53,7 @@ class MySQLSchemaOperatorE2E { boolean isLocal() { String deployment = System.getProperty("test.deployment"); boolean remote = (deployment != null && deployment.equals("remote")); - log.info("Running the operator " + (remote ? "remote" : "locally")); + log.info("Running the operator " + (remote ? "remotely" : "locally")); return !remote; } diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 61e2ecae15..54c3efc2da 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 1db82bf079..8a1cc0b00e 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index 3973379769..054626eb8b 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -22,7 +22,6 @@ public static void main(String[] args) throws IOException { Operator operator = new Operator(client); operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); - operator.installShutdownHook(); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); diff --git a/sample-operators/webpage/k8s/operator.yaml b/sample-operators/webpage/k8s/operator.yaml index f4b0b027ea..d8518ab21d 100644 --- a/sample-operators/webpage/k8s/operator.yaml +++ b/sample-operators/webpage/k8s/operator.yaml @@ -25,18 +25,22 @@ spec: imagePullPolicy: Never ports: - containerPort: 80 - readinessProbe: + startupProbe: httpGet: - path: /health + path: /startup port: 8080 initialDelaySeconds: 1 + periodSeconds: 2 timeoutSeconds: 1 + failureThreshold: 10 livenessProbe: httpGet: - path: /health + path: /healthz port: 8080 - initialDelaySeconds: 30 + initialDelaySeconds: 5 timeoutSeconds: 1 + periodSeconds: 2 + failureThreshold: 3 --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 31b9b6a5b8..0f847b8f6f 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 4.1.3-SNAPSHOT + 4.2.0-SNAPSHOT sample-webpage-operator diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/LivenessHandler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/LivenessHandler.java new file mode 100644 index 0000000000..155fc13fec --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/LivenessHandler.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; + +import io.javaoperatorsdk.operator.Operator; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import static io.javaoperatorsdk.operator.sample.StartupHandler.sendMessage; + +public class LivenessHandler implements HttpHandler { + + private final Operator operator; + + public LivenessHandler(Operator operator) { + this.operator = operator; + } + + // custom logic can be added here based on the health of event sources + @Override + public void handle(HttpExchange httpExchange) throws IOException { + if (operator.getRuntimeInfo().allEventSourcesAreHealthy()) { + sendMessage(httpExchange, 200, "healthy"); + } else { + sendMessage(httpExchange, 400, "an event source is not healthy"); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/StartupHandler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/StartupHandler.java new file mode 100644 index 0000000000..0cbc313273 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/StartupHandler.java @@ -0,0 +1,37 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import io.javaoperatorsdk.operator.Operator; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class StartupHandler implements HttpHandler { + + private final Operator operator; + + public StartupHandler(Operator operator) { + this.operator = operator; + } + + @Override + public void handle(HttpExchange httpExchange) throws IOException { + if (operator.getRuntimeInfo().isStarted()) { + sendMessage(httpExchange, 200, "started"); + } else { + sendMessage(httpExchange, 400, "not started yet"); + } + } + + public static void sendMessage(HttpExchange httpExchange, int code, String message) + throws IOException { + try (var outputStream = httpExchange.getResponseBody()) { + var bytes = message.getBytes(StandardCharsets.UTF_8); + httpExchange.sendResponseHeaders(code, bytes.length); + outputStream.write(bytes); + outputStream.flush(); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index e86bcbb120..864f97ccfe 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -1,18 +1,17 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; +import java.net.InetSocketAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.takes.facets.fork.FkRegex; -import org.takes.facets.fork.TkFork; -import org.takes.http.Exit; -import org.takes.http.FtBasic; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.javaoperatorsdk.operator.Operator; +import com.sun.net.httpserver.HttpServer; + public class WebPageOperator { public static final String WEBPAGE_RECONCILER_ENV = "WEBPAGE_RECONCILER"; public static final String WEBPAGE_CLASSIC_RECONCILER_ENV_VALUE = "classic"; @@ -24,7 +23,7 @@ public static void main(String[] args) throws IOException { log.info("WebServer Operator starting!"); KubernetesClient client = new KubernetesClientBuilder().build(); - Operator operator = new Operator(client); + Operator operator = new Operator(client, o -> o.withStopOnInformerErrorDuringStartup(false)); String reconcilerEnvVar = System.getenv(WEBPAGE_RECONCILER_ENV); if (WEBPAGE_CLASSIC_RECONCILER_ENV_VALUE.equals(reconcilerEnvVar)) { operator.register(new WebPageReconciler(client)); @@ -34,9 +33,13 @@ public static void main(String[] args) throws IOException { } else { operator.register(new WebPageStandaloneDependentsReconciler(client)); } - operator.installShutdownHook(); operator.start(); - new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + server.createContext("/startup", new StartupHandler(operator)); + // we want to restart the operator if something goes wrong with (maybe just some) event sources + server.createContext("/healthz", new LivenessHandler(operator)); + server.setExecutor(null); + server.start(); } } diff --git a/sample-operators/webpage/src/main/resources/log4j2.xml b/sample-operators/webpage/src/main/resources/log4j2.xml index 5b794e7de3..3e92919d3b 100644 --- a/sample-operators/webpage/src/main/resources/log4j2.xml +++ b/sample-operators/webpage/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ - +