From f5b30daa4b230b52fc09136610c97aecd3efb0e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 09:14:04 +0000 Subject: [PATCH 0001/1608] chore(deps): bump spring-boot.version from 2.3.4.RELEASE to 2.5.4 Bumps `spring-boot.version` from 2.3.4.RELEASE to 2.5.4. Updates `spring-boot-dependencies` from 2.3.4.RELEASE to 2.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.3.4.RELEASE...v2.5.4) Updates `spring-boot-maven-plugin` from 2.3.4.RELEASE to 2.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.3.4.RELEASE...v2.5.4) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f22a480fe6..0f5d2ecbe7 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 1.13.0 3.20.2 4.1.0 - 2.3.4.RELEASE + 2.5.4 2.11 3.8.1 From 7ba3ea9d942298e2094cdbd6a74ca23f10633b55 Mon Sep 17 00:00:00 2001 From: laxmikantbpandhare Date: Fri, 6 Aug 2021 12:00:51 -0700 Subject: [PATCH 0002/1608] feat: micrometer Integration for generating metrics Fixes #64 --- operator-framework-core/pom.xml | 12 ++ .../io/javaoperatorsdk/operator/Metrics.java | 180 ++++++++++++++++++ .../io/javaoperatorsdk/operator/Operator.java | 9 +- .../api/config/ConfigurationService.java | 5 + .../processing/DefaultEventHandler.java | 20 +- .../operator/processing/EventDispatcher.java | 15 +- .../CustomResourceSelectorTest.java | 14 +- .../processing/DefaultEventHandlerTest.java | 16 +- .../processing/EventDispatcherTest.java | 10 +- .../operator/IntegrationTestSupport.java | 2 +- 10 files changed, 270 insertions(+), 13 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index bda9f6f227..c1e487bd86 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -15,6 +15,13 @@ Core framework for implementing Kubernetes operators jar + + 11 + 11 + 11 + 1.7.3 + + @@ -92,5 +99,10 @@ log4j-core test + + io.micrometer + micrometer-core + ${micrometer-core.version} + diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java new file mode 100644 index 0000000000..6edc599e47 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java @@ -0,0 +1,180 @@ +package io.javaoperatorsdk.operator; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.DeleteControl; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.distribution.pause.PauseDetector; +import io.micrometer.core.instrument.noop.*; +import java.util.concurrent.TimeUnit; +import java.util.function.ToDoubleFunction; +import java.util.function.ToLongFunction; + +public class Metrics { + public static final Metrics NOOP = new Metrics(new NoopMeterRegistry(Clock.SYSTEM)); + private final MeterRegistry registry; + + public Metrics(MeterRegistry registry) { + this.registry = registry; + } + + public UpdateControl timeControllerCreateOrUpdate( + ResourceController controller, + ControllerConfiguration configuration, + R resource, + Context context) { + final var name = configuration.getName(); + final var timer = + Timer.builder("operator.sdk.controllers.execution.createorupdate") + .tags("controller", name) + .publishPercentiles(0.3, 0.5, 0.95) + .publishPercentileHistogram() + .register(registry); + try { + final var result = timer.record(() -> controller.createOrUpdateResource(resource, context)); + String successType = "cr"; + if (result.isUpdateStatusSubResource()) { + successType = "status"; + } + if (result.isUpdateCustomResourceAndStatusSubResource()) { + successType = "both"; + } + registry + .counter( + "operator.sdk.controllers.execution.success", "controller", name, "type", successType) + .increment(); + return result; + } catch (Exception e) { + registry + .counter( + "operator.sdk.controllers.execution.failure", + "controller", + name, + "exception", + e.getClass().getSimpleName()) + .increment(); + throw e; + } + } + + public DeleteControl timeControllerDelete( + ResourceController controller, + ControllerConfiguration configuration, + CustomResource resource, + Context context) { + final var name = configuration.getName(); + final var timer = + Timer.builder("operator.sdk.controllers.execution.delete") + .tags("controller", name) + .publishPercentiles(0.3, 0.5, 0.95) + .publishPercentileHistogram() + .register(registry); + try { + final var result = timer.record(() -> controller.deleteResource(resource, context)); + String successType = "notDelete"; + if (result == DeleteControl.DEFAULT_DELETE) { + successType = "delete"; + } + registry + .counter( + "operator.sdk.controllers.execution.success", "controller", name, "type", successType) + .increment(); + return result; + } catch (Exception e) { + registry + .counter( + "operator.sdk.controllers.execution.failure", + "controller", + name, + "exception", + e.getClass().getSimpleName()) + .increment(); + throw e; + } + } + + public void timeControllerRetry() { + + registry + .counter( + "operator.sdk.retry.on.exception", "retry", "retryCounter", "type", + "retryException") + .increment(); + + } + + public void timeControllerEvents() { + + registry + .counter( + "operator.sdk.total.events.received", "events", "totalEvents", "type", + "eventsReceived") + .increment(); + + } + + public static class NoopMeterRegistry extends MeterRegistry { + public NoopMeterRegistry(Clock clock) { + super(clock); + } + + @Override + protected Gauge newGauge(Meter.Id id, T t, ToDoubleFunction toDoubleFunction) { + return new NoopGauge(id); + } + + @Override + protected Counter newCounter(Meter.Id id) { + return new NoopCounter(id); + } + + @Override + protected Timer newTimer( + Meter.Id id, + DistributionStatisticConfig distributionStatisticConfig, + PauseDetector pauseDetector) { + return new NoopTimer(id); + } + + @Override + protected DistributionSummary newDistributionSummary( + Meter.Id id, DistributionStatisticConfig distributionStatisticConfig, double v) { + return new NoopDistributionSummary(id); + } + + @Override + protected Meter newMeter(Meter.Id id, Meter.Type type, Iterable iterable) { + return new NoopMeter(id); + } + + @Override + protected FunctionTimer newFunctionTimer( + Meter.Id id, + T t, + ToLongFunction toLongFunction, + ToDoubleFunction toDoubleFunction, + TimeUnit timeUnit) { + return new NoopFunctionTimer(id); + } + + @Override + protected FunctionCounter newFunctionCounter( + Meter.Id id, T t, ToDoubleFunction toDoubleFunction) { + return new NoopFunctionCounter(id); + } + + @Override + protected TimeUnit getBaseTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + protected DistributionStatisticConfig defaultHistogramConfig() { + return DistributionStatisticConfig.NONE; + } + } +} 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 3fa1c5f7e6..930db02e73 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 @@ -25,14 +25,17 @@ public class Operator implements AutoCloseable { private final Object lock; private final List controllers; private volatile boolean started; + private final Metrics metrics; - public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) { + public Operator( + KubernetesClient k8sClient, ConfigurationService configurationService, Metrics metrics) { this.k8sClient = k8sClient; this.configurationService = configurationService; this.closeables = new ArrayList<>(); this.lock = new Object(); this.controllers = new ArrayList<>(); this.started = false; + this.metrics = metrics; } /** Adds a shutdown hook that automatically calls {@link #close()} when the app shuts down. */ @@ -40,6 +43,10 @@ public void installShutdownHook() { Runtime.getRuntime().addShutdownHook(new Thread(this::close)); } + public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) { + this(k8sClient, configurationService, Metrics.NOOP); + } + public KubernetesClient getKubernetesClient() { return k8sClient; } 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 7482080148..126bb46b93 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 @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.ResourceController; import java.util.Set; @@ -93,4 +94,8 @@ default ObjectMapper getObjectMapper() { default int getTerminationTimeoutSeconds() { return DEFAULT_TERMINATION_TIMEOUT_SECONDS; } + + default Metrics getMetrics() { + return Metrics.NOOP; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index ddd6cc8e4b..c59ca2dc9e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; @@ -16,6 +17,7 @@ import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.processing.retry.Retry; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; +import io.micrometer.core.instrument.Clock; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -46,6 +48,7 @@ public class DefaultEventHandler implements EventHandler { private final int terminationTimeout; private final ReentrantLock lock = new ReentrantLock(); private DefaultEventSourceManager eventSourceManager; + private ControllerConfiguration configuration; public DefaultEventHandler( ResourceController controller, ControllerConfiguration configuration, MixedOperation client) { @@ -54,20 +57,20 @@ public DefaultEventHandler( configuration.getName(), GenericRetry.fromConfiguration(configuration.getRetryConfiguration()), configuration.getConfigurationService().concurrentReconciliationThreads(), - configuration.getConfigurationService().getTerminationTimeoutSeconds()); + configuration.getConfigurationService().getTerminationTimeoutSeconds(), configuration); } DefaultEventHandler( EventDispatcher eventDispatcher, String relatedControllerName, Retry retry, - int concurrentReconciliationThreads) { + int concurrentReconciliationThreads, ControllerConfiguration configuration) { this( eventDispatcher, relatedControllerName, retry, concurrentReconciliationThreads, - ConfigurationService.DEFAULT_TERMINATION_TIMEOUT_SECONDS); + ConfigurationService.DEFAULT_TERMINATION_TIMEOUT_SECONDS, configuration); } private DefaultEventHandler( @@ -75,7 +78,7 @@ private DefaultEventHandler( String relatedControllerName, Retry retry, int concurrentReconciliationThreads, - int terminationTimeout) { + int terminationTimeout, ControllerConfiguration configuration) { this.eventDispatcher = eventDispatcher; this.retry = retry; this.controllerName = relatedControllerName; @@ -85,6 +88,7 @@ private DefaultEventHandler( new ScheduledThreadPoolExecutor( concurrentReconciliationThreads, runnable -> new Thread(runnable, "EventHandler-" + relatedControllerName)); + this.configuration = configuration; } @Override @@ -113,6 +117,10 @@ public void handleEvent(Event event) { final Predicate selector = event.getCustomResourcesSelector(); for (String uid : eventSourceManager.getLatestResourceUids(selector)) { eventBuffer.addEvent(uid, event); + configuration + .getConfigurationService() + .getMetrics() + .timeControllerEvents(); executeBufferedEvents(uid); } } finally { @@ -162,6 +170,10 @@ void eventProcessingFinished( if (retry != null && postExecutionControl.exceptionDuringExecution()) { handleRetryOnException(executionScope); + configuration + .getConfigurationService() + .getMetrics() + .timeControllerRetry(); return; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 382a973ea5..e69d69ffb7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -123,7 +123,12 @@ private PostExecutionControl handleCreateOrUpdate( getName(resource), getVersion(resource), executionScope); - UpdateControl updateControl = controller.createOrUpdateResource(resource, context); + + UpdateControl updateControl = + configuration + .getConfigurationService() + .getMetrics() + .timeControllerCreateOrUpdate(controller, configuration, resource, context); R updatedCustomResource = null; if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); @@ -153,8 +158,12 @@ private PostExecutionControl handleDelete(R resource, Context context) { "Executing delete for resource: {} with version: {}", getName(resource), getVersion(resource)); - // todo: this is be executed in a try-catch statement, in case this fails - DeleteControl deleteControl = controller.deleteResource(resource, context); + + DeleteControl deleteControl = + configuration + .getConfigurationService() + .getMetrics() + .timeControllerDelete(controller, configuration, resource, context); final var useFinalizer = configuration.useFinalizer(); if (useFinalizer) { if (deleteControl == DeleteControl.DEFAULT_DELETE diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java index 5bc2eb20e9..72c681b857 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java @@ -11,10 +11,13 @@ import static org.mockito.Mockito.when; import io.fabric8.kubernetes.client.Watcher; +import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import java.util.Objects; import java.util.UUID; @@ -33,12 +36,17 @@ class CustomResourceSelectorTest { private final DefaultEventSourceManager defaultEventSourceManagerMock = mock(DefaultEventSourceManager.class); + private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); + private ControllerConfiguration configuration = + mock(ControllerConfiguration.class); + private final ConfigurationService configService = mock(ConfigurationService.class); + private final DefaultEventHandler defaultEventHandler = new DefaultEventHandler( eventDispatcherMock, "Test", null, - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); + ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER, configuration); @BeforeEach public void setup() { @@ -58,6 +66,10 @@ public void setup() { }) .when(defaultEventSourceManagerMock) .cleanup(any()); + + when(configuration.getName()).thenReturn("DefaultEventHandlerTest"); + when(configService.getMetrics()).thenReturn(Metrics.NOOP); + when(configuration.getConfigurationService()).thenReturn(configService); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index b4bd89dd18..699784180f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -13,8 +13,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.Watcher; +import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; @@ -44,20 +47,25 @@ class DefaultEventHandlerTest { mock(DefaultEventSourceManager.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); + private ControllerConfiguration configuration = + mock(ControllerConfiguration.class); + private final ConfigurationService configService = mock(ConfigurationService.class); private DefaultEventHandler defaultEventHandler = new DefaultEventHandler( eventDispatcherMock, "Test", null, - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); + ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER, + configuration); private DefaultEventHandler defaultEventHandlerWithRetry = new DefaultEventHandler( eventDispatcherMock, "Test", GenericRetry.defaultLimitedExponentialRetry(), - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); + ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER, + configuration); @BeforeEach public void setup() { @@ -66,6 +74,10 @@ public void setup() { defaultEventHandler.setEventSourceManager(defaultEventSourceManagerMock); defaultEventHandlerWithRetry.setEventSourceManager(defaultEventSourceManagerMock); + when(configuration.getName()).thenReturn("DefaultEventHandlerTest"); + when(configService.getMetrics()).thenReturn(Metrics.NOOP); + when(configuration.getConfigurationService()).thenReturn(configService); + // todo: remove when(defaultEventSourceManagerMock.getCache()).thenReturn(customResourceCache); doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResource(any()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index 1d00ebfcac..fa44508227 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -15,12 +15,14 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.Watcher; +import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.DeleteControl; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; @@ -40,6 +42,7 @@ class EventDispatcherTest { private final ResourceController controller = mock(ResourceController.class); private ControllerConfiguration configuration = mock(ControllerConfiguration.class); + private final ConfigurationService configService = mock(ConfigurationService.class); private final EventDispatcher.CustomResourceFacade customResourceFacade = mock(EventDispatcher.CustomResourceFacade.class); @@ -51,6 +54,9 @@ void setup() { when(configuration.getFinalizer()).thenReturn(DEFAULT_FINALIZER); when(configuration.useFinalizer()).thenCallRealMethod(); + when(configuration.getName()).thenReturn("EventDispatcherTestController"); + when(configService.getMetrics()).thenReturn(Metrics.NOOP); + when(configuration.getConfigurationService()).thenReturn(configService); when(controller.createOrUpdateResource(eq(testCustomResource), any())) .thenReturn(UpdateControl.updateCustomResource(testCustomResource)); when(controller.deleteResource(eq(testCustomResource), any())) @@ -135,7 +141,6 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { @Test void callDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { configureToNotUseFinalizer(); - markForDeletion(testCustomResource); eventDispatcher.handleExecution( @@ -156,6 +161,9 @@ void doNotCallDeleteIfMarkedForDeletionWhenFinalizerHasAlreadyBeenRemoved() { private void configureToNotUseFinalizer() { ControllerConfiguration configuration = mock(ControllerConfiguration.class); + when(configuration.getName()).thenReturn("EventDispatcherTestController"); + when(configService.getMetrics()).thenReturn(Metrics.NOOP); + when(configuration.getConfigurationService()).thenReturn(configService); when(configuration.useFinalizer()).thenReturn(false); eventDispatcher = new EventDispatcher(controller, configuration, customResourceFacade); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java index fc1cd343cb..bbdb510d8e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java @@ -59,7 +59,7 @@ public void initialize(KubernetesClient k8sClient, ResourceController controller namespaces.create( new NamespaceBuilder().withNewMetadata().withName(TEST_NAMESPACE).endMetadata().build()); } - operator = new Operator(k8sClient, configurationService); + operator = new Operator(k8sClient, configurationService, Metrics.NOOP); final var overriddenConfig = ControllerConfigurationOverrider.override(config).settingNamespace(TEST_NAMESPACE); if (retry != null) { From 13505167e55485e1a1ad3cda6270c4712c267241 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 18:32:17 +0200 Subject: [PATCH 0003/1608] refactor: make default ObjectMapper constant --- .../operator/api/config/ConfigurationService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 126bb46b93..b834b32f21 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 @@ -10,6 +10,8 @@ /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { + ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + /** * Retrieves the configuration associated with the specified controller * @@ -80,7 +82,7 @@ default int concurrentReconciliationThreads() { * @return the ObjectMapper to use */ default ObjectMapper getObjectMapper() { - return new ObjectMapper(); + return OBJECT_MAPPER; } int DEFAULT_TERMINATION_TIMEOUT_SECONDS = 10; From b8433ed97343eba9a8fd121a5e58e298829ba189 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Mon, 30 Aug 2021 11:00:04 +0200 Subject: [PATCH 0004/1608] chore: cleanup operator-framework-core pom (#518) --- operator-framework-core/pom.xml | 19 ++++++------------- pom.xml | 6 ++++++ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index c1e487bd86..c66adb0b55 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -15,19 +15,11 @@ Core framework for implementing Kubernetes operators jar - - 11 - 11 - 11 - 1.7.3 - - org.apache.maven.plugins maven-surefire-plugin - ${maven-surefire-plugin.version} @@ -59,14 +51,20 @@ + io.fabric8 openshift-client + org.slf4j slf4j-api + + io.micrometer + micrometer-core + org.junit.jupiter @@ -99,10 +97,5 @@ log4j-core test - - io.micrometer - micrometer-core - ${micrometer-core.version} - diff --git a/pom.xml b/pom.xml index 0f5d2ecbe7..183436f4c7 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,7 @@ 3.20.2 4.1.0 2.5.4 + 1.7.3 2.11 3.8.1 @@ -110,6 +111,11 @@ compile-testing ${compile-testing.version} + + io.micrometer + micrometer-core + ${micrometer-core.version} + com.squareup From cb7608e03e9d8a335f112c183186363cdb26279d Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Thu, 19 Aug 2021 13:52:22 +0200 Subject: [PATCH 0005/1608] feat: option to limit CRs the operator watches #453 --- operator-framework-core/pom.xml | 13 +- .../operator/api/Controller.java | 9 + .../api/config/AbstractConfiguration.java | 48 +++++ .../AbstractControllerConfiguration.java | 10 +- .../api/config/AnnotationConfiguration.java | 36 ++++ .../api/config/ControllerConfiguration.java | 2 + .../ControllerConfigurationOverrider.java | 10 +- .../internal/CustomResourceEventSource.java | 17 +- .../CustomResourceEventSourceTest.java | 6 +- .../internal/CustomResourceSelectorTest.java | 195 ++++++++++++++++++ .../runtime/AnnotationConfiguration.java | 69 +------ .../runtime/DefaultConfigurationService.java | 4 +- 12 files changed, 348 insertions(+), 71 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfiguration.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index c66adb0b55..68213a7907 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -50,13 +50,14 @@ + io.fabric8 openshift-client - + org.slf4j slf4j-api @@ -97,5 +98,15 @@ log4j-core test + + io.fabric8 + kubernetes-server-mock + test + + + org.awaitility + awaitility + test + diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java index 90bd70d8b3..4c332ebf24 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java @@ -40,4 +40,13 @@ * @return the list of namespaces this controller monitors */ String[] namespaces() default {}; + + /** + * Optional label selector used to identify the set of custom resources the controller will acc + * upon. The label selector can be made of multiple comma separated requirements that acts as a + * logical AND operator. + * + * @return the finalizer name + */ + String labelSelector() default NULL; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfiguration.java new file mode 100644 index 0000000000..2524098218 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfiguration.java @@ -0,0 +1,48 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.api.ResourceController; + +public abstract class AbstractConfiguration + implements ControllerConfiguration { + private final Class> controllerClass; + private final Class customResourceClass; + private ConfigurationService service; + + public AbstractConfiguration( + Class> controllerClass, Class customResourceClass) { + this.controllerClass = controllerClass; + this.customResourceClass = customResourceClass; + } + + @Override + public String getName() { + return ControllerUtils.getNameFor(controllerClass); + } + + @Override + public String getCRDName() { + return CustomResource.getCRDName(getCustomResourceClass()); + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public void setConfigurationService(ConfigurationService service) { + this.service = service; + } + + @Override + public String getAssociatedControllerClassName() { + return controllerClass.getCanonicalName(); + } + + @Override + public Class getCustomResourceClass() { + return customResourceClass; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java index 2bd7c4448d..91f5b68a41 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java @@ -15,6 +15,7 @@ public abstract class AbstractControllerConfiguration private final Set namespaces; private final boolean watchAllNamespaces; private final RetryConfiguration retryConfiguration; + private final String labelSelector; private ConfigurationService service; public AbstractControllerConfiguration( @@ -24,7 +25,8 @@ public AbstractControllerConfiguration( String finalizer, boolean generationAware, Set namespaces, - RetryConfiguration retryConfiguration) { + RetryConfiguration retryConfiguration, + String labelSelector) { this.associatedControllerClassName = associatedControllerClassName; this.name = name; this.crdName = crdName; @@ -37,6 +39,7 @@ public AbstractControllerConfiguration( retryConfiguration == null ? ControllerConfiguration.super.getRetryConfiguration() : retryConfiguration; + this.labelSelector = labelSelector; } @Override @@ -88,4 +91,9 @@ public ConfigurationService getConfigurationService() { public void setConfigurationService(ConfigurationService service) { this.service = service; } + + @Override + public String getLabelSelector() { + return labelSelector; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfiguration.java new file mode 100644 index 0000000000..99265b58de --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfiguration.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.api.ResourceController; +import java.util.Optional; +import java.util.function.Predicate; + +public class AnnotationConfiguration extends AbstractConfiguration { + private final Optional annotation; + + public AnnotationConfiguration( + Class> controllerClass, Class customResourceClass) { + super(controllerClass, customResourceClass); + this.annotation = Optional.ofNullable(controllerClass.getAnnotation(Controller.class)); + } + + @Override + public String getFinalizer() { + return annotation + .map(Controller::finalizerName) + .filter(Predicate.not(String::isBlank)) + .orElse(ControllerUtils.getDefaultFinalizerName(getCRDName())); + } + + @Override + public boolean isGenerationAware() { + return annotation.map(Controller::generationAwareEventProcessing).orElse(true); + } + + @Override + public String getLabelSelector() { + return annotation.map(Controller::labelSelector).orElse(""); + } +} 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 95555c511d..2525f5bbc0 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 @@ -13,6 +13,8 @@ public interface ControllerConfiguration { String getFinalizer(); + String getLabelSelector(); + boolean isGenerationAware(); Class getCustomResourceClass(); 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 10743fba7f..cc6d5f75ec 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 @@ -11,6 +11,7 @@ public class ControllerConfigurationOverrider { private boolean generationAware; private Set namespaces; private RetryConfiguration retry; + private String labelSelector; private final ControllerConfiguration original; private ControllerConfigurationOverrider(ControllerConfiguration original) { @@ -18,6 +19,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { generationAware = original.isGenerationAware(); namespaces = new HashSet<>(original.getNamespaces()); retry = original.getRetryConfiguration(); + labelSelector = original.getLabelSelector(); this.original = original; } @@ -57,6 +59,11 @@ public ControllerConfigurationOverrider withRetry(RetryConfiguration retry) { return this; } + public ControllerConfigurationOverrider withLabelSelector(String labelSelector) { + this.labelSelector = labelSelector; + return this; + } + public ControllerConfiguration build() { return new AbstractControllerConfiguration( original.getAssociatedControllerClassName(), @@ -65,7 +72,8 @@ public ControllerConfiguration build() { finalizer, generationAware, namespaces, - retry) { + retry, + labelSelector) { @Override public Class getCustomResourceClass() { return original.getCustomResourceClass(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index c2538fda3a..728b52823c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -5,12 +5,14 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.ListOptions; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.WatcherException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.utils.Utils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.CustomResourceCache; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; @@ -33,6 +35,7 @@ public class CustomResourceEventSource> extends A private final Set targetNamespaces; private final boolean generationAware; private final String resourceFinalizer; + private final String labelSelector; private final Map lastGenerationProcessedSuccessfully = new ConcurrentHashMap<>(); private final List watches; private final String resClass; @@ -46,6 +49,7 @@ public CustomResourceEventSource( configuration.getEffectiveNamespaces(), configuration.isGenerationAware(), configuration.getFinalizer(), + configuration.getLabelSelector(), configuration.getCustomResourceClass(), new CustomResourceCache(configuration.getConfigurationService().getObjectMapper())); } @@ -55,12 +59,14 @@ public CustomResourceEventSource( Set targetNamespaces, boolean generationAware, String resourceFinalizer, + String labelSelector, Class resClass) { this( client, targetNamespaces, generationAware, resourceFinalizer, + labelSelector, resClass, new CustomResourceCache()); } @@ -70,12 +76,14 @@ public CustomResourceEventSource( Set targetNamespaces, boolean generationAware, String resourceFinalizer, + String labelSelector, Class resClass, CustomResourceCache customResourceCache) { this.client = client; this.targetNamespaces = targetNamespaces; this.generationAware = generationAware; this.resourceFinalizer = resourceFinalizer; + this.labelSelector = labelSelector; this.watches = new ArrayList<>(); this.resClass = resClass.getName(); this.customResourceCache = customResourceCache; @@ -83,14 +91,19 @@ public CustomResourceEventSource( @Override public void start() { + var options = new ListOptions(); + if (Utils.isNotNullOrEmpty(labelSelector)) { + options.setLabelSelector(labelSelector); + } + if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { - var w = client.inAnyNamespace().watch(this); + var w = client.inAnyNamespace().watch(options, this); watches.add(w); log.debug("Registered controller {} -> {} for any namespace", resClass, w); } else { targetNamespaces.forEach( ns -> { - var w = client.inNamespace(ns).watch(this); + var w = client.inNamespace(ns).watch(options, this); watches.add(w); log.debug("Registered controller {} -> {} for namespace: {}", resClass, w, ns); }); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index da8639b1a0..1e1a3513b5 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -25,7 +25,8 @@ class CustomResourceEventSourceTest { EventHandler eventHandler = mock(EventHandler.class); private CustomResourceEventSource customResourceEventSource = - new CustomResourceEventSource<>(client, null, true, FINALIZER, TestCustomResource.class); + new CustomResourceEventSource<>( + client, null, true, FINALIZER, null, TestCustomResource.class); @BeforeEach public void setup() { @@ -72,7 +73,8 @@ public void normalExecutionIfGenerationChanges() { @Test public void handlesAllEventIfNotGenerationAware() { customResourceEventSource = - new CustomResourceEventSource<>(client, null, false, FINALIZER, TestCustomResource.class); + new CustomResourceEventSource<>( + client, null, false, FINALIZER, null, TestCustomResource.class); setup(); TestCustomResource customResource1 = TestUtils.testCustomResource(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java new file mode 100644 index 0000000000..d81e588eed --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -0,0 +1,195 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.VersionInfo; +import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; +import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.config.AbstractConfiguration; +import io.javaoperatorsdk.operator.api.config.AnnotationConfiguration; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.Version; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import java.text.ParseException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import org.awaitility.core.ConditionTimeoutException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@EnableKubernetesMockClient(crud = true, https = false) +public class CustomResourceSelectorTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomResourceSelectorTest.class); + + KubernetesMockServer server; + KubernetesClient client; + ConfigurationService configurationService; + + @SuppressWarnings("unchecked") + @BeforeEach + void setUpResources() throws ParseException { + String buildDate = + DateTimeFormatter.ofPattern(VersionInfo.VersionKeys.BUILD_DATE_FORMAT) + .format(LocalDateTime.now()); + + server + .expect() + .get() + .withPath("/version") + .andReturn( + 200, + new VersionInfo.Builder() + .withBuildDate(buildDate) + .withMajor("1") + .withMinor("21") + .build()) + .always(); + + configurationService = spy(ConfigurationService.class); + when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); + when(configurationService.getVersion()).thenReturn(new Version("1", "1", new Date())); + when(configurationService.getConfigurationFor(any(ResourceController.class))) + .thenAnswer( + invocation -> { + var answer = + new AnnotationConfiguration<>(MyController.class, TestCustomResource.class); + answer.setConfigurationService(configurationService); + return answer; + }); + } + + @Test + void resourceWatchedByLabel() { + assertThat(server).isNotNull(); + assertThat(client).isNotNull(); + + try (Operator o1 = new Operator(client, configurationService); + Operator o2 = new Operator(client, configurationService)) { + + AtomicInteger c1 = new AtomicInteger(); + AtomicInteger c1err = new AtomicInteger(); + AtomicInteger c2 = new AtomicInteger(); + AtomicInteger c2err = new AtomicInteger(); + + o1.register( + new MyController( + resource -> { + if ("foo".equals(resource.getMetadata().getName())) { + c1.incrementAndGet(); + } + if ("bar".equals(resource.getMetadata().getName())) { + c1err.incrementAndGet(); + } + }), + new MyConfiguration(configurationService, "app=foo")); + o1.start(); + o2.register( + new MyController( + resource -> { + if ("bar".equals(resource.getMetadata().getName())) { + c2.incrementAndGet(); + } + if ("foo".equals(resource.getMetadata().getName())) { + c2err.incrementAndGet(); + } + }), + new MyConfiguration(configurationService, "app=bar")); + o2.start(); + + client.resources(TestCustomResource.class).inNamespace("test").create(newMyResource("foo")); + client.resources(TestCustomResource.class).inNamespace("test").create(newMyResource("bar")); + + await() + .atMost(5, TimeUnit.SECONDS) + .pollInterval(100, TimeUnit.MILLISECONDS) + .until(() -> c1.get() == 1 && c1err.get() == 0); + await() + .atMost(5, TimeUnit.SECONDS) + .pollInterval(100, TimeUnit.MILLISECONDS) + .until(() -> c2.get() == 1 && c2err.get() == 0); + + assertThrows( + ConditionTimeoutException.class, + () -> await().atMost(2, TimeUnit.SECONDS).untilAtomic(c1err, is(greaterThan(0)))); + assertThrows( + ConditionTimeoutException.class, + () -> await().atMost(2, TimeUnit.SECONDS).untilAtomic(c2err, is(greaterThan(0)))); + } + } + + public TestCustomResource newMyResource(String app) { + TestCustomResource resource = new TestCustomResource(); + resource.setMetadata(new ObjectMetaBuilder().withName(app).addToLabels("app", app).build()); + return resource; + } + + public static class MyConfiguration extends AbstractConfiguration { + + private final String labelSelector; + + public MyConfiguration(ConfigurationService configurationService, String labelSelector) { + super(MyController.class, TestCustomResource.class); + this.labelSelector = labelSelector; + + setConfigurationService(configurationService); + } + + @Override + public String getFinalizer() { + return ControllerUtils.getDefaultFinalizerName(getCRDName()); + } + + @Override + public String getLabelSelector() { + return labelSelector; + } + + @Override + public boolean isGenerationAware() { + return true; + } + } + + @Controller + public static class MyController implements ResourceController { + + private final Consumer consumer; + + public MyController(Consumer consumer) { + this.consumer = consumer; + } + + @Override + public UpdateControl createOrUpdateResource( + TestCustomResource resource, Context context) { + + LOGGER.info("Received event on: {}", resource); + + consumer.accept(resource); + + return UpdateControl.updateStatusSubResource(resource); + } + } +} diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 81f6c521b8..3c688eb631 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,72 +1,17 @@ package io.javaoperatorsdk.operator.config.runtime; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; +/** @deprecated use {@link io.javaoperatorsdk.operator.api.config.AnnotationConfiguration} */ +@Deprecated +@SuppressWarnings("unchecked") public class AnnotationConfiguration - implements ControllerConfiguration { - - private final ResourceController controller; - private final Optional annotation; - private ConfigurationService service; + extends io.javaoperatorsdk.operator.api.config.AnnotationConfiguration { public AnnotationConfiguration(ResourceController controller) { - this.controller = controller; - this.annotation = Optional.ofNullable(controller.getClass().getAnnotation(Controller.class)); - } - - @Override - public String getName() { - return ControllerUtils.getNameFor(controller); - } - - @Override - public String getCRDName() { - return CustomResource.getCRDName(getCustomResourceClass()); - } - - @Override - public String getFinalizer() { - return annotation - .map(Controller::finalizerName) - .filter(Predicate.not(String::isBlank)) - .orElse(ControllerUtils.getDefaultFinalizerName(getCRDName())); - } - - @Override - public boolean isGenerationAware() { - return annotation.map(Controller::generationAwareEventProcessing).orElse(true); - } - - @Override - public Class getCustomResourceClass() { - return RuntimeControllerMetadata.getCustomResourceClass(controller); - } - - @Override - public Set getNamespaces() { - return Set.of(annotation.map(Controller::namespaces).orElse(new String[] {})); - } - - @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public void setConfigurationService(ConfigurationService service) { - this.service = service; - } - - @Override - public String getAssociatedControllerClassName() { - return controller.getClass().getCanonicalName(); + super( + (Class>) controller.getClass(), + RuntimeControllerMetadata.getCustomResourceClass(controller)); } } 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 013f52855a..590b9fb746 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 @@ -29,8 +29,8 @@ ControllerConfiguration getConfigurationFor( var config = super.getConfigurationFor(controller); if (config == null) { if (createIfNeeded) { - // create the the configuration on demand and register it - config = new AnnotationConfiguration(controller); + // create the configuration on demand and register it + config = new AnnotationConfiguration<>(controller); register(config); log.info( "Created configuration for controller {} with name {}", From 9a0068960f421caed5ab960cca7025103c1481b8 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 30 Aug 2021 16:54:24 +0200 Subject: [PATCH 0006/1608] feat: make ControllerConfiguration easier to implement for simple cases Now providing default implementation for most methods. Removed confusing AbstractConfiguration and AnnotationConfiguration classes. --- .../api/config/AbstractConfiguration.java | 48 -------------- .../api/config/AnnotationConfiguration.java | 36 ---------- .../api/config/ControllerConfiguration.java | 29 +++++++-- .../config/ControllerConfigurationTest.java | 25 +++++++ .../internal/CustomResourceSelectorTest.java | 33 ++++------ .../runtime/AnnotationConfiguration.java | 65 +++++++++++++++++-- 6 files changed, 117 insertions(+), 119 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfiguration.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfiguration.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfiguration.java deleted file mode 100644 index 2524098218..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfiguration.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.ResourceController; - -public abstract class AbstractConfiguration - implements ControllerConfiguration { - private final Class> controllerClass; - private final Class customResourceClass; - private ConfigurationService service; - - public AbstractConfiguration( - Class> controllerClass, Class customResourceClass) { - this.controllerClass = controllerClass; - this.customResourceClass = customResourceClass; - } - - @Override - public String getName() { - return ControllerUtils.getNameFor(controllerClass); - } - - @Override - public String getCRDName() { - return CustomResource.getCRDName(getCustomResourceClass()); - } - - @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public void setConfigurationService(ConfigurationService service) { - this.service = service; - } - - @Override - public String getAssociatedControllerClassName() { - return controllerClass.getCanonicalName(); - } - - @Override - public Class getCustomResourceClass() { - return customResourceClass; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfiguration.java deleted file mode 100644 index 99265b58de..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import java.util.Optional; -import java.util.function.Predicate; - -public class AnnotationConfiguration extends AbstractConfiguration { - private final Optional annotation; - - public AnnotationConfiguration( - Class> controllerClass, Class customResourceClass) { - super(controllerClass, customResourceClass); - this.annotation = Optional.ofNullable(controllerClass.getAnnotation(Controller.class)); - } - - @Override - public String getFinalizer() { - return annotation - .map(Controller::finalizerName) - .filter(Predicate.not(String::isBlank)) - .orElse(ControllerUtils.getDefaultFinalizerName(getCRDName())); - } - - @Override - public boolean isGenerationAware() { - return annotation.map(Controller::generationAwareEventProcessing).orElse(true); - } - - @Override - public String getLabelSelector() { - return annotation.map(Controller::labelSelector).orElse(""); - } -} 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 2525f5bbc0..9c28800b9f 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 @@ -1,23 +1,38 @@ package io.javaoperatorsdk.operator.api.config; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.api.Controller; +import java.lang.reflect.ParameterizedType; import java.util.Collections; import java.util.Set; public interface ControllerConfiguration { - String getName(); + default String getName() { + return ControllerUtils.getDefaultResourceControllerName(getAssociatedControllerClassName()); + } - String getCRDName(); + default String getCRDName() { + return CustomResource.getCRDName(getCustomResourceClass()); + } - String getFinalizer(); + default String getFinalizer() { + return ControllerUtils.getDefaultFinalizerName(getCRDName()); + } - String getLabelSelector(); + default String getLabelSelector() { + return null; + } - boolean isGenerationAware(); + default boolean isGenerationAware() { + return true; + } - Class getCustomResourceClass(); + default Class getCustomResourceClass() { + ParameterizedType type = (ParameterizedType) getClass().getGenericInterfaces()[0]; + return (Class) type.getActualTypeArguments()[0]; + } String getAssociatedControllerClassName(); @@ -69,7 +84,7 @@ default RetryConfiguration getRetryConfiguration() { ConfigurationService getConfigurationService(); - void setConfigurationService(ConfigurationService service); + default void setConfigurationService(ConfigurationService service) {} default boolean useFinalizer() { return !Controller.NO_FINALIZER.equals(getFinalizer()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java new file mode 100644 index 0000000000..1b58b9e9e7 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -0,0 +1,25 @@ +package io.javaoperatorsdk.operator.api.config; + +import static org.junit.jupiter.api.Assertions.*; + +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import org.junit.jupiter.api.Test; + +class ControllerConfigurationTest { + + @Test + void getCustomResourceClass() { + final ControllerConfiguration conf = new ControllerConfiguration<>() { + @Override + public String getAssociatedControllerClassName() { + return null; + } + + @Override + public ConfigurationService getConfigurationService() { + return null; + } + }; + assertEquals(TestCustomResource.class, conf.getCustomResourceClass()); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index d81e588eed..74be731334 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -14,15 +14,13 @@ import io.fabric8.kubernetes.client.VersionInfo; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; -import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; -import io.javaoperatorsdk.operator.api.config.AbstractConfiguration; -import io.javaoperatorsdk.operator.api.config.AnnotationConfiguration; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Version; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import java.text.ParseException; @@ -70,14 +68,8 @@ void setUpResources() throws ParseException { configurationService = spy(ConfigurationService.class); when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); when(configurationService.getVersion()).thenReturn(new Version("1", "1", new Date())); - when(configurationService.getConfigurationFor(any(ResourceController.class))) - .thenAnswer( - invocation -> { - var answer = - new AnnotationConfiguration<>(MyController.class, TestCustomResource.class); - answer.setConfigurationService(configurationService); - return answer; - }); + when(configurationService.getConfigurationFor(any(MyController.class))).thenReturn( + new MyConfiguration(configurationService, null)); } @Test @@ -145,30 +137,29 @@ public TestCustomResource newMyResource(String app) { return resource; } - public static class MyConfiguration extends AbstractConfiguration { + public static class MyConfiguration implements ControllerConfiguration { private final String labelSelector; + private final ConfigurationService service; public MyConfiguration(ConfigurationService configurationService, String labelSelector) { - super(MyController.class, TestCustomResource.class); this.labelSelector = labelSelector; - - setConfigurationService(configurationService); + service = configurationService; } @Override - public String getFinalizer() { - return ControllerUtils.getDefaultFinalizerName(getCRDName()); + public String getLabelSelector() { + return labelSelector; } @Override - public String getLabelSelector() { - return labelSelector; + public String getAssociatedControllerClassName() { + return MyController.class.getCanonicalName(); } @Override - public boolean isGenerationAware() { - return true; + public ConfigurationService getConfigurationService() { + return service; } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 3c688eb631..dc03670ff6 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,17 +1,68 @@ package io.javaoperatorsdk.operator.config.runtime; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; -/** @deprecated use {@link io.javaoperatorsdk.operator.api.config.AnnotationConfiguration} */ -@Deprecated -@SuppressWarnings("unchecked") public class AnnotationConfiguration - extends io.javaoperatorsdk.operator.api.config.AnnotationConfiguration { + implements ControllerConfiguration { + + private final ResourceController controller; + private final Optional annotation; + private ConfigurationService service; public AnnotationConfiguration(ResourceController controller) { - super( - (Class>) controller.getClass(), - RuntimeControllerMetadata.getCustomResourceClass(controller)); + this.controller = controller; + this.annotation = Optional.ofNullable(controller.getClass().getAnnotation(Controller.class)); + } + + @Override + public String getName() { + return ControllerUtils.getNameFor(controller); + } + + @Override + public String getFinalizer() { + return annotation + .map(Controller::finalizerName) + .filter(Predicate.not(String::isBlank)) + .orElse(ControllerUtils.getDefaultFinalizerName(getCRDName())); + } + + @Override + public boolean isGenerationAware() { + return annotation.map(Controller::generationAwareEventProcessing).orElse(true); + } + + @Override + public Class getCustomResourceClass() { + return RuntimeControllerMetadata.getCustomResourceClass(controller); + } + + @Override + public Set getNamespaces() { + return Set.of(annotation.map(Controller::namespaces).orElse(new String[] {})); + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public void setConfigurationService(ConfigurationService service) { + this.service = service; + } + + @Override + public String getAssociatedControllerClassName() { + return controller.getClass().getCanonicalName(); } } + From 92b5e4ee862a897434d299ad2916e1370549598f Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 30 Aug 2021 17:27:56 +0200 Subject: [PATCH 0007/1608] chore(docs): add description for getLabelSelector method --- .../operator/api/config/ControllerConfiguration.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 9c28800b9f..89d4872303 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 @@ -21,6 +21,14 @@ default String getFinalizer() { return ControllerUtils.getDefaultFinalizerName(getCRDName()); } + /** + * Retrieves the label selector that is used to filter which custom resources are actually watched + * by the associated controller. See + * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on + * syntax. + * + * @return the label selector filtering watched custom resources + */ default String getLabelSelector() { return null; } From 1df1e0f22865914f29a5cb15b7a47627f985b526 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 31 Aug 2021 11:01:44 +0200 Subject: [PATCH 0008/1608] fix: do not break backward compatibility --- .../AbstractControllerConfiguration.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java index 91f5b68a41..d6e6371750 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java @@ -42,6 +42,24 @@ public AbstractControllerConfiguration( this.labelSelector = labelSelector; } + /** + * @deprecated use + * {@link #AbstractControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration, String)} + * instead + */ + @Deprecated + public AbstractControllerConfiguration( + String associatedControllerClassName, + String name, + String crdName, + String finalizer, + boolean generationAware, + Set namespaces, + RetryConfiguration retryConfiguration) { + this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, + retryConfiguration, null); + } + @Override public String getName() { return name; From 2c9e1510f475c5ebb7e3e0449a8785dac76bd7e0 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 11:40:44 +0200 Subject: [PATCH 0009/1608] refactor: make Operator only deal with ConfiguredController management Introduced ConfiguredController to gather in one entity ResourceController, its associated configuration and Kubernetes client so that they can be passed around more easily in an always-associated manner (as, right now, configuration can be disassociated from the controller). Another aspect is that it allows us to simplify the management of the lifecycle as ConfiguredControllers can be started/stopped and the Operator doesn't need to bother about the details. --- .../io/javaoperatorsdk/operator/Operator.java | 136 +++------------ .../processing/ConfiguredController.java | 164 ++++++++++++++++++ 2 files changed, 184 insertions(+), 116 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java 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 930db02e73..0480b61fc0 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 @@ -1,29 +1,28 @@ package io.javaoperatorsdk.operator; -import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; -import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.Version; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Objects; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.Version; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.ConfiguredController; + @SuppressWarnings("rawtypes") public class Operator implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient k8sClient; private final ConfigurationService configurationService; - private final List closeables; private final Object lock; - private final List controllers; + private final List controllers; private volatile boolean started; private final Metrics metrics; @@ -31,7 +30,6 @@ public Operator( KubernetesClient k8sClient, ConfigurationService configurationService, Metrics metrics) { this.k8sClient = k8sClient; this.configurationService = configurationService; - this.closeables = new ArrayList<>(); this.lock = new Object(); this.controllers = new ArrayList<>(); this.started = false; @@ -89,9 +87,7 @@ public void start() { throw new OperatorException("Error retrieving the server version", e); } - for (ControllerRef ref : controllers) { - startController(ref.controller, ref.configuration); - } + controllers.forEach(ConfiguredController::start); started = true; } @@ -108,7 +104,7 @@ public void close() { log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); - for (Closeable closeable : this.closeables) { + for (Closeable closeable : this.controllers) { try { log.debug("closing {}", closeable); closeable.close(); @@ -150,32 +146,6 @@ public void register(ResourceController controller public void register( ResourceController controller, ControllerConfiguration configuration) throws OperatorException { - synchronized (lock) { - if (!started) { - this.controllers.add(new ControllerRef(controller, configuration)); - } else { - this.controllers.add(new ControllerRef(controller, configuration)); - startController(controller, configuration); - } - } - } - - /** - * Registers the specified controller with this operator, overriding its default configuration by - * the specified one (usually created via - * {@link io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider#override(ControllerConfiguration)}, - * passing it the controller's original configuration. - * - * @param controller the controller to register - * @param configuration the configuration with which we want to register the controller, if {@code - * null}, the controller's original configuration is used - * @param the {@code CustomResource} type associated with the controller - * @throws OperatorException if a problem occurred during the registration process - */ - private void startController( - ResourceController controller, ControllerConfiguration configuration) - throws OperatorException { - final var existing = configurationService.getConfigurationFor(controller); if (existing == null) { log.warn( @@ -188,39 +158,13 @@ private void startController( if (configuration == null) { configuration = existing; } - - final Class resClass = configuration.getCustomResourceClass(); - final String controllerName = configuration.getName(); - final var crdName = configuration.getCRDName(); - final var specVersion = "v1"; - - // check that the custom resource is known by the cluster if configured that way - final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config - if (configurationService.checkCRDAndValidateLocalModel()) { - crd = k8sClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get(); - if (crd == null) { - throwMissingCRDException(crdName, specVersion, controllerName); + synchronized (lock) { + final var configuredController = + new ConfiguredController(controller, configuration, k8sClient); + this.controllers.add(configuredController); + if (started) { + configuredController.start(); } - - // Apply validations that are not handled by fabric8 - CustomResourceUtils.assertCustomResource(resClass, crd); - } - - try { - DefaultEventSourceManager eventSourceManager = - new DefaultEventSourceManager( - controller, configuration, k8sClient.customResources(resClass)); - controller.init(eventSourceManager); - closeables.add(eventSourceManager); - } catch (MissingCRDException e) { - throwMissingCRDException(crdName, specVersion, controllerName); - } - - if (failOnMissingCurrentNS(configuration)) { - throw new OperatorException( - "Controller '" - + controllerName - + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } final var watchedNS = @@ -229,49 +173,9 @@ private void startController( : configuration.getEffectiveNamespaces(); log.info( "Registered Controller: '{}' for CRD: '{}' for namespace(s): {}", - controllerName, - resClass, + configuration.getName(), + configuration.getCustomResourceClass(), watchedNS); } } - - private void throwMissingCRDException(String crdName, String specVersion, String controllerName) { - throw new MissingCRDException( - crdName, - specVersion, - "'" - + crdName - + "' " - + specVersion - + " CRD was not found on the cluster, controller '" - + controllerName - + "' cannot be registered"); - } - - /** - * Determines whether we should fail because the current namespace is request as target namespace - * but is missing - * - * @return {@code true} if the current namespace is requested but is missing, {@code false} - * otherwise - */ - private static boolean failOnMissingCurrentNS( - ControllerConfiguration configuration) { - if (configuration.watchCurrentNamespace()) { - final var effectiveNamespaces = configuration.getEffectiveNamespaces(); - return effectiveNamespaces.size() == 1 - && effectiveNamespaces.stream().allMatch(Objects::isNull); - } - return false; - } - - private static class ControllerRef { - public final ResourceController controller; - public final ControllerConfiguration configuration; - - public ControllerRef(ResourceController controller, ControllerConfiguration configuration) { - this.controller = controller; - this.configuration = configuration; - } - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java new file mode 100644 index 0000000000..e1ea258995 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -0,0 +1,164 @@ +package io.javaoperatorsdk.operator.processing; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.CustomResourceUtils; +import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.DeleteControl; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; + +public class ConfiguredController> implements ResourceController, + Closeable { + private final ResourceController controller; + private final ControllerConfiguration configuration; + private final KubernetesClient k8sClient; + private EventSourceManager manager; + + public ConfiguredController(ResourceController controller, + ControllerConfiguration configuration, + KubernetesClient k8sClient) { + this.controller = controller; + this.configuration = configuration; + this.k8sClient = k8sClient; + } + + @Override + public DeleteControl deleteResource(R resource, Context context) { + return controller.deleteResource(resource, context); + } + + @Override + public UpdateControl createOrUpdateResource(R resource, Context context) { + return controller.createOrUpdateResource(resource, context); + } + + @Override + public void init(EventSourceManager eventSourceManager) { + this.manager = eventSourceManager; + controller.init(eventSourceManager); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ConfiguredController that = (ConfiguredController) o; + return configuration.getName().equals(that.configuration.getName()); + } + + @Override + public int hashCode() { + return configuration.getName().hashCode(); + } + + @Override + public String toString() { + return "'" + configuration.getName() + "' Controller"; + } + + public ResourceController getController() { + return controller; + } + + public ControllerConfiguration getConfiguration() { + return configuration; + } + + public KubernetesClient getClient() { + return k8sClient; + } + + /** + * Registers the specified controller with this operator, overriding its default configuration by + * the specified one (usually created via + * {@link io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider#override(ControllerConfiguration)}, + * passing it the controller's original configuration. + * + * @throws OperatorException if a problem occurred during the registration process + */ + public void start() throws OperatorException { + final Class resClass = configuration.getCustomResourceClass(); + final String controllerName = configuration.getName(); + final var crdName = configuration.getCRDName(); + final var specVersion = "v1"; + + // check that the custom resource is known by the cluster if configured that way + final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config + if (configuration.getConfigurationService().checkCRDAndValidateLocalModel()) { + crd = k8sClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get(); + if (crd == null) { + throwMissingCRDException(crdName, specVersion, controllerName); + } + + // Apply validations that are not handled by fabric8 + CustomResourceUtils.assertCustomResource(resClass, crd); + } + + try { + DefaultEventSourceManager eventSourceManager = + new DefaultEventSourceManager( + controller, configuration, k8sClient.customResources(resClass)); + controller.init(eventSourceManager); + } catch (MissingCRDException e) { + throwMissingCRDException(crdName, specVersion, controllerName); + } + + if (failOnMissingCurrentNS()) { + throw new OperatorException( + "Controller '" + + controllerName + + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); + } + } + + private void throwMissingCRDException(String crdName, String specVersion, String controllerName) { + throw new MissingCRDException( + crdName, + specVersion, + "'" + + crdName + + "' " + + specVersion + + " CRD was not found on the cluster, controller '" + + controllerName + + "' cannot be registered"); + } + + /** + * Determines whether we should fail because the current namespace is request as target namespace + * but is missing + * + * @return {@code true} if the current namespace is requested but is missing, {@code false} + * otherwise + */ + private boolean failOnMissingCurrentNS() { + if (configuration.watchCurrentNamespace()) { + final var effectiveNamespaces = configuration.getEffectiveNamespaces(); + return effectiveNamespaces.size() == 1 + && effectiveNamespaces.stream().allMatch(Objects::isNull); + } + return false; + } + + + @Override + public void close() throws IOException { + manager.close(); + } +} From e701bdd0c5f77a5e5e9025f2d25b3a0f5a176f56 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 11:45:20 +0200 Subject: [PATCH 0010/1608] fix: use resources method instead of deprecated customResources --- .../operator/processing/ConfiguredController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index e1ea258995..7037d181ed 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -113,7 +113,7 @@ public void start() throws OperatorException { try { DefaultEventSourceManager eventSourceManager = new DefaultEventSourceManager( - controller, configuration, k8sClient.customResources(resClass)); + controller, configuration, k8sClient.resources(resClass)); controller.init(eventSourceManager); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); From e137cce78f1123a0136d4fdf06baecfce18a6fb1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 12:25:10 +0200 Subject: [PATCH 0011/1608] feat: add direct access to custom resource client --- .../operator/processing/ConfiguredController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index 7037d181ed..da5f8401f4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -1,5 +1,8 @@ package io.javaoperatorsdk.operator.processing; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; import java.io.Closeable; import java.io.IOException; import java.util.Objects; @@ -84,6 +87,10 @@ public KubernetesClient getClient() { return k8sClient; } + public MixedOperation, Resource> getCRClient() { + return k8sClient.resources(configuration.getCustomResourceClass()); + } + /** * Registers the specified controller with this operator, overriding its default configuration by * the specified one (usually created via From 1181af23d80f55ea689b96005d8182882bb529b1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 12:26:25 +0200 Subject: [PATCH 0012/1608] refactor: propagate use of ConfiguredController, generify some --- .../processing/ConfiguredController.java | 10 ++- .../processing/DefaultEventHandler.java | 62 +++++++++---------- .../operator/processing/EventDispatcher.java | 44 ++++++------- .../event/DefaultEventSourceManager.java | 38 ++++++------ .../processing/EventDispatcherTest.java | 25 +++++--- 5 files changed, 90 insertions(+), 89 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index da5f8401f4..7f75cc4920 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -1,15 +1,15 @@ package io.javaoperatorsdk.operator.processing; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; import java.io.Closeable; import java.io.IOException; import java.util.Objects; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; @@ -118,9 +118,7 @@ public void start() throws OperatorException { } try { - DefaultEventSourceManager eventSourceManager = - new DefaultEventSourceManager( - controller, configuration, k8sClient.resources(resClass)); + DefaultEventSourceManager eventSourceManager = new DefaultEventSourceManager<>(this); controller.init(eventSourceManager); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index c59ca2dc9e..0648c14b25 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -4,20 +4,7 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; -import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.javaoperatorsdk.operator.Metrics; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.RetryInfo; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.processing.retry.GenericRetry; -import io.javaoperatorsdk.operator.processing.retry.Retry; -import io.javaoperatorsdk.operator.processing.retry.RetryExecution; -import io.micrometer.core.instrument.Clock; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -27,41 +14,52 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.RetryInfo; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.processing.retry.Retry; +import io.javaoperatorsdk.operator.processing.retry.RetryExecution; + /** * Event handler that makes sure that events are processed in a "single threaded" way per resource * UID, while buffering events which are received during an execution. */ -public class DefaultEventHandler implements EventHandler { +public class DefaultEventHandler> implements EventHandler { private static final Logger log = LoggerFactory.getLogger(DefaultEventHandler.class); private final EventBuffer eventBuffer; private final Set underProcessing = new HashSet<>(); private final ScheduledThreadPoolExecutor executor; - private final EventDispatcher eventDispatcher; + private final EventDispatcher eventDispatcher; private final Retry retry; private final Map retryState = new HashMap<>(); private final String controllerName; private final int terminationTimeout; private final ReentrantLock lock = new ReentrantLock(); - private DefaultEventSourceManager eventSourceManager; - private ControllerConfiguration configuration; + private DefaultEventSourceManager eventSourceManager; + private final ControllerConfiguration configuration; - public DefaultEventHandler( - ResourceController controller, ControllerConfiguration configuration, MixedOperation client) { + public DefaultEventHandler(ConfiguredController controller) { this( - new EventDispatcher(controller, configuration, client), - configuration.getName(), - GenericRetry.fromConfiguration(configuration.getRetryConfiguration()), - configuration.getConfigurationService().concurrentReconciliationThreads(), - configuration.getConfigurationService().getTerminationTimeoutSeconds(), configuration); + new EventDispatcher<>(controller), + controller.getConfiguration().getName(), + GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), + controller.getConfiguration().getConfigurationService().concurrentReconciliationThreads(), + controller.getConfiguration().getConfigurationService().getTerminationTimeoutSeconds(), + controller.getConfiguration()); } DefaultEventHandler( - EventDispatcher eventDispatcher, + EventDispatcher eventDispatcher, String relatedControllerName, Retry retry, int concurrentReconciliationThreads, ControllerConfiguration configuration) { @@ -74,7 +72,7 @@ public DefaultEventHandler( } private DefaultEventHandler( - EventDispatcher eventDispatcher, + EventDispatcher eventDispatcher, String relatedControllerName, Retry retry, int concurrentReconciliationThreads, @@ -104,7 +102,7 @@ public void close() { } } - public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { + public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { this.eventSourceManager = eventSourceManager; } @@ -159,7 +157,7 @@ private RetryInfo retryInfo(String customResourceUid) { } void eventProcessingFinished( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { + ExecutionScope executionScope, PostExecutionControl postExecutionControl) { try { lock.lock(); log.debug( @@ -223,7 +221,7 @@ private void handleRetryOnException(ExecutionScope executionScope) { }); } - private void markSuccessfulExecutionRegardingRetry(ExecutionScope executionScope) { + private void markSuccessfulExecutionRegardingRetry(ExecutionScope executionScope) { log.debug( "Marking successful execution for resource: {}", getName(executionScope.getCustomResource())); @@ -233,7 +231,7 @@ private void markSuccessfulExecutionRegardingRetry(ExecutionScope executionScope .cancelOnceSchedule(executionScope.getCustomResourceUid()); } - private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) { + private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) { RetryExecution retryExecution = retryState.get(executionScope.getCustomResourceUid()); if (retryExecution == null) { retryExecution = retry.initExecution(); @@ -259,9 +257,9 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) { * would override an additional change coming from a different client. */ private void cacheUpdatedResourceIfChanged( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { + ExecutionScope executionScope, PostExecutionControl postExecutionControl) { if (postExecutionControl.customResourceUpdatedDuringExecution()) { - CustomResource originalCustomResource = executionScope.getCustomResource(); + R originalCustomResource = executionScope.getCustomResource(); CustomResource customResourceAfterExecution = postExecutionControl.getUpdatedCustomResource().get(); String originalResourceVersion = getVersion(originalCustomResource); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index e69d69ffb7..b0d0678d5c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -5,6 +5,9 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; @@ -17,32 +20,25 @@ import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.EventList; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Dispatches events to the Controller and handles Finalizers for a single type of Custom Resource. */ -class EventDispatcher { +class EventDispatcher> { private static final Logger log = LoggerFactory.getLogger(EventDispatcher.class); - private final ResourceController controller; - private final ControllerConfiguration configuration; + private final ConfiguredController controller; private final CustomResourceFacade customResourceFacade; - EventDispatcher( - ResourceController controller, - ControllerConfiguration configuration, + EventDispatcher(ConfiguredController controller, CustomResourceFacade customResourceFacade) { this.controller = controller; this.customResourceFacade = customResourceFacade; - this.configuration = configuration; } - public EventDispatcher( - ResourceController controller, ControllerConfiguration configuration, MixedOperation client) { - this(controller, configuration, new CustomResourceFacade<>(client)); + public EventDispatcher(ConfiguredController controller) { + this(controller, new CustomResourceFacade<>(controller.getCRClient())); } public PostExecutionControl handleExecution(ExecutionScope executionScope) { @@ -92,6 +88,10 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) { } } + private ControllerConfiguration configuration() { + return controller.getConfiguration(); + } + /** * Determines whether the given resource should be dispatched to the controller's * {@link ResourceController#deleteResource(CustomResource, Context)} method @@ -103,12 +103,12 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) { private boolean shouldNotDispatchToDelete(R resource) { // we don't dispatch to delete if the controller is configured to use a finalizer but that // finalizer is not present (which means it's already been removed) - return configuration.useFinalizer() && !resource.hasFinalizer(configuration.getFinalizer()); + return configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer()); } private PostExecutionControl handleCreateOrUpdate( ExecutionScope executionScope, R resource, Context context) { - if (configuration.useFinalizer() && !resource.hasFinalizer(configuration.getFinalizer())) { + if (configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer())) { /* * We always add the finalizer if missing and the controller is configured to use a finalizer. * We execute the controller processing only for processing the event sent as a results of the @@ -125,10 +125,10 @@ private PostExecutionControl handleCreateOrUpdate( executionScope); UpdateControl updateControl = - configuration + configuration() .getConfigurationService() .getMetrics() - .timeControllerCreateOrUpdate(controller, configuration, resource, context); + .timeControllerCreateOrUpdate(controller, configuration(), resource, context); R updatedCustomResource = null; if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); @@ -160,14 +160,14 @@ private PostExecutionControl handleDelete(R resource, Context context) { getVersion(resource)); DeleteControl deleteControl = - configuration + configuration() .getConfigurationService() .getMetrics() - .timeControllerDelete(controller, configuration, resource, context); - final var useFinalizer = configuration.useFinalizer(); + .timeControllerDelete(controller, configuration(), resource, context); + final var useFinalizer = configuration().useFinalizer(); if (useFinalizer) { if (deleteControl == DeleteControl.DEFAULT_DELETE - && resource.hasFinalizer(configuration.getFinalizer())) { + && resource.hasFinalizer(configuration().getFinalizer())) { R customResource = removeFinalizer(resource); // todo: should we patch the resource to remove the finalizer instead of updating it return PostExecutionControl.customResourceUpdated(customResource); @@ -185,7 +185,7 @@ private PostExecutionControl handleDelete(R resource, Context context) { private void updateCustomResourceWithFinalizer(R resource) { log.debug( "Adding finalizer for resource: {} version: {}", getUID(resource), getVersion(resource)); - resource.addFinalizer(configuration.getFinalizer()); + resource.addFinalizer(configuration().getFinalizer()); replace(resource); } @@ -200,7 +200,7 @@ private R removeFinalizer(R resource) { "Removing finalizer on resource: {} with version: {}", getUID(resource), getVersion(resource)); - resource.removeFinalizer(configuration.getFinalizer()); + resource.removeFinalizer(configuration().getFinalizer()); return customResourceFacade.replaceWithLock(resource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 3fc060e716..961a4c6c1f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -1,16 +1,5 @@ package io.javaoperatorsdk.operator.processing.event; -import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.javaoperatorsdk.operator.MissingCRDException; -import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.CustomResourceCache; -import io.javaoperatorsdk.operator.processing.DefaultEventHandler; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import java.util.Collections; import java.util.List; import java.util.Map; @@ -20,10 +9,22 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class DefaultEventSourceManager implements EventSourceManager { +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.CustomResourceCache; +import io.javaoperatorsdk.operator.processing.DefaultEventHandler; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; + +public class DefaultEventSourceManager> + implements EventSourceManager { public static final String RETRY_TIMER_EVENT_SOURCE_NAME = "retry-timer-event-source"; private static final String CUSTOM_RESOURCE_EVENT_SOURCE_NAME = "custom-resource-event-source"; @@ -31,10 +32,10 @@ public class DefaultEventSourceManager implements EventSourceManager { private final ReentrantLock lock = new ReentrantLock(); private final Map eventSources = new ConcurrentHashMap<>(); - private final DefaultEventHandler defaultEventHandler; + private final DefaultEventHandler defaultEventHandler; private TimerEventSource retryTimerEventSource; - DefaultEventSourceManager(DefaultEventHandler defaultEventHandler, boolean supportRetry) { + DefaultEventSourceManager(DefaultEventHandler defaultEventHandler, boolean supportRetry) { this.defaultEventHandler = defaultEventHandler; defaultEventHandler.setEventSourceManager(this); if (supportRetry) { @@ -43,12 +44,11 @@ public class DefaultEventSourceManager implements EventSourceManager { } } - @SuppressWarnings({"rawtypes", "unchecked"}) - public > DefaultEventSourceManager( - ResourceController controller, ControllerConfiguration configuration, MixedOperation client) { - this(new DefaultEventHandler(controller, configuration, client), true); + public DefaultEventSourceManager(ConfiguredController controller) { + this(new DefaultEventHandler<>(controller), true); registerEventSource( - CUSTOM_RESOURCE_EVENT_SOURCE_NAME, new CustomResourceEventSource<>(client, configuration)); + CUSTOM_RESOURCE_EVENT_SOURCE_NAME, + new CustomResourceEventSource<>(controller.getCRClient(), controller.getConfiguration())); } @Override diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index fa44508227..721aaaa7ef 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -13,6 +13,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.Watcher; import io.javaoperatorsdk.operator.Metrics; @@ -26,13 +35,6 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; class EventDispatcherTest { @@ -40,15 +42,17 @@ class EventDispatcherTest { private CustomResource testCustomResource; private EventDispatcher eventDispatcher; private final ResourceController controller = mock(ResourceController.class); - private ControllerConfiguration configuration = + private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); private final ConfigurationService configService = mock(ConfigurationService.class); + private final ConfiguredController> configuredController = + new ConfiguredController(controller, configuration, null); private final EventDispatcher.CustomResourceFacade customResourceFacade = mock(EventDispatcher.CustomResourceFacade.class); @BeforeEach void setup() { - eventDispatcher = new EventDispatcher(controller, configuration, customResourceFacade); + eventDispatcher = new EventDispatcher(configuredController, customResourceFacade); testCustomResource = TestUtils.testCustomResource(); @@ -165,7 +169,8 @@ private void configureToNotUseFinalizer() { when(configService.getMetrics()).thenReturn(Metrics.NOOP); when(configuration.getConfigurationService()).thenReturn(configService); when(configuration.useFinalizer()).thenReturn(false); - eventDispatcher = new EventDispatcher(controller, configuration, customResourceFacade); + eventDispatcher = new EventDispatcher(new ConfiguredController(controller, configuration, null), + customResourceFacade); } @Test From bf7ad38f5678825c79153d5881c27f5c2afec3fb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 18:03:37 +0200 Subject: [PATCH 0013/1608] fix: avoid crash if for some reason the build time is unavailable --- .../java/io/javaoperatorsdk/operator/api/config/Utils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 d47837593b..83a537cedc 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 @@ -1,11 +1,11 @@ package io.javaoperatorsdk.operator.api.config; import java.io.IOException; -import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; import java.util.Properties; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +41,8 @@ public static Version loadFromProperties() { // RFC 822 date is the default format used by git-commit-id-plugin new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .parse(properties.getProperty("git.build.time")); - } catch (ParseException e) { + } catch (Exception e) { + log.debug("Couldn't parse git.build.time property", e); builtTime = Date.from(Instant.EPOCH); } return new Version( From e685c4ffa48cd0c11b0b7356a009877315a784ba Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 22:19:02 +0200 Subject: [PATCH 0014/1608] feat: use ConfiguredController --- .../internal/CustomResourceEventSource.java | 94 +++++-------------- .../CustomResourceEventSourceTest.java | 62 ++++++++++-- 2 files changed, 78 insertions(+), 78 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 728b52823c..a6e532c73b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -4,26 +4,25 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.ListOptions; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.WatcherException; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.utils.Utils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.CustomResourceCache; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** This is a special case since is not bound to a single custom resource */ public class CustomResourceEventSource> extends AbstractEventSource @@ -31,66 +30,24 @@ public class CustomResourceEventSource> extends A private static final Logger log = LoggerFactory.getLogger(CustomResourceEventSource.class); - private final MixedOperation, Resource> client; - private final Set targetNamespaces; - private final boolean generationAware; - private final String resourceFinalizer; - private final String labelSelector; + private final ConfiguredController controller; private final Map lastGenerationProcessedSuccessfully = new ConcurrentHashMap<>(); private final List watches; - private final String resClass; private final CustomResourceCache customResourceCache; - public CustomResourceEventSource( - MixedOperation, Resource> client, - ControllerConfiguration configuration) { - this( - client, - configuration.getEffectiveNamespaces(), - configuration.isGenerationAware(), - configuration.getFinalizer(), - configuration.getLabelSelector(), - configuration.getCustomResourceClass(), - new CustomResourceCache(configuration.getConfigurationService().getObjectMapper())); - } - - CustomResourceEventSource( - MixedOperation, Resource> client, - Set targetNamespaces, - boolean generationAware, - String resourceFinalizer, - String labelSelector, - Class resClass) { - this( - client, - targetNamespaces, - generationAware, - resourceFinalizer, - labelSelector, - resClass, - new CustomResourceCache()); - } - - CustomResourceEventSource( - MixedOperation, Resource> client, - Set targetNamespaces, - boolean generationAware, - String resourceFinalizer, - String labelSelector, - Class resClass, - CustomResourceCache customResourceCache) { - this.client = client; - this.targetNamespaces = targetNamespaces; - this.generationAware = generationAware; - this.resourceFinalizer = resourceFinalizer; - this.labelSelector = labelSelector; - this.watches = new ArrayList<>(); - this.resClass = resClass.getName(); - this.customResourceCache = customResourceCache; + public CustomResourceEventSource(ConfiguredController controller) { + this.controller = controller; + this.watches = new LinkedList<>(); + this.customResourceCache = new CustomResourceCache( + controller.getConfiguration().getConfigurationService().getObjectMapper()); } @Override public void start() { + final var configuration = controller.getConfiguration(); + final var targetNamespaces = configuration.getEffectiveNamespaces(); + final var client = controller.getCRClient(); + final var labelSelector = configuration.getLabelSelector(); var options = new ListOptions(); if (Utils.isNotNullOrEmpty(labelSelector)) { options.setLabelSelector(labelSelector); @@ -99,13 +56,13 @@ public void start() { if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { var w = client.inAnyNamespace().watch(options, this); watches.add(w); - log.debug("Registered controller {} -> {} for any namespace", resClass, w); + log.debug("Registered {} -> {} for any namespace", controller, w); } else { targetNamespaces.forEach( ns -> { var w = client.inNamespace(ns).watch(options, this); watches.add(w); - log.debug("Registered controller {} -> {} for namespace: {}", resClass, w, ns); + log.debug("Registered {} -> {} for namespace: {}", controller, w, ns); }); } } @@ -115,10 +72,10 @@ public void close() { eventHandler.close(); for (Watch watch : this.watches) { try { - log.info("Closing watch {} -> {}", resClass, watch); + log.info("Closing watch {} -> {}", controller, watch); watch.close(); } catch (Exception e) { - log.warn("Error closing watcher {} -> {}", resClass, watch, e); + log.warn("Error closing watcher {} -> {}", controller, watch, e); } } } @@ -152,14 +109,15 @@ public void eventReceived(Watcher.Action action, T customResource) { } private void markLastGenerationProcessed(T resource) { - if (generationAware && resource.hasFinalizer(resourceFinalizer)) { + if (controller.getConfiguration().isGenerationAware() + && resource.hasFinalizer(controller.getConfiguration().getFinalizer())) { lastGenerationProcessedSuccessfully.put( KubernetesResourceUtils.getUID(resource), resource.getMetadata().getGeneration()); } } private boolean skipBecauseOfGeneration(T customResource) { - if (!generationAware) { + if (!controller.getConfiguration().isGenerationAware()) { return false; } // if CR being deleted generation is naturally not changing, so we process all the events diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 1e1a3513b5..08f4174023 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -4,29 +4,35 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import java.time.LocalDateTime; +import java.util.Arrays; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import java.time.LocalDateTime; -import java.util.Arrays; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; class CustomResourceEventSourceTest { public static final String FINALIZER = "finalizer"; - MixedOperation, Resource> client = + private static final MixedOperation, Resource> client = mock(MixedOperation.class); EventHandler eventHandler = mock(EventHandler.class); private CustomResourceEventSource customResourceEventSource = - new CustomResourceEventSource<>( - client, null, true, FINALIZER, null, TestCustomResource.class); + new CustomResourceEventSource<>(new TestConfiguredController(true)); @BeforeEach public void setup() { @@ -36,7 +42,7 @@ public void setup() { @Test public void skipsEventHandlingIfGenerationNotIncreased() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResource1.getMetadata().setFinalizers(Arrays.asList(FINALIZER)); + customResource1.getMetadata().setFinalizers(List.of(FINALIZER)); customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); verify(eventHandler, times(1)).handleEvent(any()); @@ -73,8 +79,7 @@ public void normalExecutionIfGenerationChanges() { @Test public void handlesAllEventIfNotGenerationAware() { customResourceEventSource = - new CustomResourceEventSource<>( - client, null, false, FINALIZER, null, TestCustomResource.class); + new CustomResourceEventSource<>(new TestConfiguredController(false)); setup(); TestCustomResource customResource1 = TestUtils.testCustomResource(); @@ -96,4 +101,41 @@ public void eventNotMarkedForLastGenerationIfNoFinalizer() { customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); verify(eventHandler, times(2)).handleEvent(any()); } + + private static class TestConfiguredController extends ConfiguredController { + + public TestConfiguredController(boolean generationAware) { + super(null, new TestConfiguration(generationAware), null); + } + + @Override + public MixedOperation, Resource> getCRClient() { + return client; + } + } + private static class TestConfiguration implements + ControllerConfiguration { + + final ConfigurationService service = mock(ConfigurationService.class); + final boolean generationAware; + public TestConfiguration(boolean generationAware) { + when(service.getObjectMapper()).thenReturn(ConfigurationService.OBJECT_MAPPER); + this.generationAware = generationAware; + } + + @Override + public String getAssociatedControllerClassName() { + return null; + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public boolean isGenerationAware() { + return generationAware; + } + } } From 67ea87c33dcf2b8485e20026212cc721df9fc784 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 22:34:15 +0200 Subject: [PATCH 0015/1608] refactor: use non-deprecated replaceStatus method --- .../io/javaoperatorsdk/operator/processing/EventDispatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index b0d0678d5c..6b0c2cbc16 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -227,7 +227,7 @@ public R updateStatus(R resource) { return resourceOperation .inNamespace(resource.getMetadata().getNamespace()) .withName(getName(resource)) - .updateStatus(resource); + .replaceStatus(resource); } public R replaceWithLock(R resource) { From 4a6080bf7c4231c89df89664df8a3fdfe1cfe2c7 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 22:34:36 +0200 Subject: [PATCH 0016/1608] refactor: more generification --- .../AbstractControllerConfiguration.java | 5 +++-- .../ControllerConfigurationOverrider.java | 11 +++++----- .../processing/DefaultEventHandler.java | 21 ++++++++----------- .../operator/processing/EventDispatcher.java | 10 ++++----- .../processing/ExecutionConsumer.java | 21 ++++++++----------- .../processing/PostExecutionControl.java | 16 +++++++------- .../event/DefaultEventSourceManager.java | 5 ++--- 7 files changed, 43 insertions(+), 46 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java index d6e6371750..6db3d682d6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java @@ -1,10 +1,11 @@ package io.javaoperatorsdk.operator.api.config; -import io.fabric8.kubernetes.client.CustomResource; import java.util.Collections; import java.util.Set; -public abstract class AbstractControllerConfiguration +import io.fabric8.kubernetes.client.CustomResource; + +public abstract class AbstractControllerConfiguration> implements ControllerConfiguration { private final String associatedControllerClassName; 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 cc6d5f75ec..7882ccf340 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,15 +1,16 @@ package io.javaoperatorsdk.operator.api.config; -import io.fabric8.kubernetes.client.CustomResource; import java.util.HashSet; import java.util.List; import java.util.Set; -public class ControllerConfigurationOverrider { +import io.fabric8.kubernetes.client.CustomResource; + +public class ControllerConfigurationOverrider> { private String finalizer; private boolean generationAware; - private Set namespaces; + private final Set namespaces; private RetryConfiguration retry; private String labelSelector; private final ControllerConfiguration original; @@ -65,7 +66,7 @@ public ControllerConfigurationOverrider withLabelSelector(String labelSelecto } public ControllerConfiguration build() { - return new AbstractControllerConfiguration( + return new AbstractControllerConfiguration<>( original.getAssociatedControllerClassName(), original.getName(), original.getCRDName(), @@ -86,7 +87,7 @@ public ConfigurationService getConfigurationService() { }; } - public static ControllerConfigurationOverrider override( + public static > ControllerConfigurationOverrider override( ControllerConfiguration original) { return new ControllerConfigurationOverrider<>(original); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 0648c14b25..312d35cae8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -4,7 +4,6 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -21,6 +20,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -46,7 +46,7 @@ public class DefaultEventHandler> implements Even private final int terminationTimeout; private final ReentrantLock lock = new ReentrantLock(); private DefaultEventSourceManager eventSourceManager; - private final ControllerConfiguration configuration; + private final ControllerConfiguration configuration; public DefaultEventHandler(ConfiguredController controller) { this( @@ -62,7 +62,7 @@ public DefaultEventHandler(ConfiguredController controller) { EventDispatcher eventDispatcher, String relatedControllerName, Retry retry, - int concurrentReconciliationThreads, ControllerConfiguration configuration) { + int concurrentReconciliationThreads, ControllerConfiguration configuration) { this( eventDispatcher, relatedControllerName, @@ -76,7 +76,7 @@ private DefaultEventHandler( String relatedControllerName, Retry retry, int concurrentReconciliationThreads, - int terminationTimeout, ControllerConfiguration configuration) { + int terminationTimeout, ControllerConfiguration configuration) { this.eventDispatcher = eventDispatcher; this.retry = retry; this.controllerName = relatedControllerName; @@ -157,7 +157,7 @@ private RetryInfo retryInfo(String customResourceUid) { } void eventProcessingFinished( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { + ExecutionScope executionScope, PostExecutionControl postExecutionControl) { try { lock.lock(); log.debug( @@ -194,7 +194,7 @@ void eventProcessingFinished( * events (received meanwhile retry is in place or already in buffer) instantly or always wait * according to the retry timing if there was an exception. */ - private void handleRetryOnException(ExecutionScope executionScope) { + private void handleRetryOnException(ExecutionScope executionScope) { RetryExecution execution = getOrInitRetryExecution(executionScope); boolean newEventsExists = eventBuffer.newEventsExists(executionScope.getCustomResourceUid()); eventBuffer.putBackEvents(executionScope.getCustomResourceUid(), executionScope.getEvents()); @@ -216,9 +216,7 @@ private void handleRetryOnException(ExecutionScope executionScope) { .getRetryTimerEventSource() .scheduleOnce(executionScope.getCustomResource(), delay); }, - () -> { - log.error("Exhausted retries for {}", executionScope); - }); + () -> log.error("Exhausted retries for {}", executionScope)); } private void markSuccessfulExecutionRegardingRetry(ExecutionScope executionScope) { @@ -257,11 +255,10 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) * would override an additional change coming from a different client. */ private void cacheUpdatedResourceIfChanged( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { + ExecutionScope executionScope, PostExecutionControl postExecutionControl) { if (postExecutionControl.customResourceUpdatedDuringExecution()) { R originalCustomResource = executionScope.getCustomResource(); - CustomResource customResourceAfterExecution = - postExecutionControl.getUpdatedCustomResource().get(); + R customResourceAfterExecution = postExecutionControl.getUpdatedCustomResource().get(); String originalResourceVersion = getVersion(originalCustomResource); log.debug( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 6b0c2cbc16..58e1a3fdd7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -41,7 +41,7 @@ public EventDispatcher(ConfiguredController controller) { this(controller, new CustomResourceFacade<>(controller.getCRClient())); } - public PostExecutionControl handleExecution(ExecutionScope executionScope) { + public PostExecutionControl handleExecution(ExecutionScope executionScope) { try { return handleDispatch(executionScope); } catch (KubernetesClientException e) { @@ -57,7 +57,7 @@ public PostExecutionControl handleExecution(ExecutionScope executionScope) { } } - private PostExecutionControl handleDispatch(ExecutionScope executionScope) { + private PostExecutionControl handleDispatch(ExecutionScope executionScope) { R resource = executionScope.getCustomResource(); log.debug("Handling events: {} for resource {}", executionScope.getEvents(), getName(resource)); @@ -106,7 +106,7 @@ private boolean shouldNotDispatchToDelete(R resource) { return configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer()); } - private PostExecutionControl handleCreateOrUpdate( + private PostExecutionControl handleCreateOrUpdate( ExecutionScope executionScope, R resource, Context context) { if (configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer())) { /* @@ -153,7 +153,7 @@ private PostExecutionControl handleCreateOrUpdate( } } - private PostExecutionControl handleDelete(R resource, Context context) { + private PostExecutionControl handleDelete(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), @@ -213,7 +213,7 @@ private R replace(R resource) { } // created to support unit testing - static class CustomResourceFacade { + static class CustomResourceFacade> { private final MixedOperation, Resource> resourceOperation; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionConsumer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionConsumer.java index 2b66ad68d4..f648b82955 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionConsumer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionConsumer.java @@ -1,20 +1,17 @@ package io.javaoperatorsdk.operator.processing; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.client.CustomResource; -class ExecutionConsumer implements Runnable { +class ExecutionConsumer> implements Runnable { - private static final Logger log = LoggerFactory.getLogger(ExecutionConsumer.class); - - private final ExecutionScope executionScope; - private final EventDispatcher eventDispatcher; - private final DefaultEventHandler defaultEventHandler; + private final ExecutionScope executionScope; + private final EventDispatcher eventDispatcher; + private final DefaultEventHandler defaultEventHandler; ExecutionConsumer( - ExecutionScope executionScope, - EventDispatcher eventDispatcher, - DefaultEventHandler defaultEventHandler) { + ExecutionScope executionScope, + EventDispatcher eventDispatcher, + DefaultEventHandler defaultEventHandler) { this.executionScope = executionScope; this.eventDispatcher = eventDispatcher; this.defaultEventHandler = defaultEventHandler; @@ -22,7 +19,7 @@ class ExecutionConsumer implements Runnable { @Override public void run() { - PostExecutionControl postExecutionControl = eventDispatcher.handleExecution(executionScope); + PostExecutionControl postExecutionControl = eventDispatcher.handleExecution(executionScope); defaultEventHandler.eventProcessingFinished(executionScope, postExecutionControl); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java index 1e0c82f1b2..b36bcedd90 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java @@ -1,19 +1,20 @@ package io.javaoperatorsdk.operator.processing; -import io.fabric8.kubernetes.client.CustomResource; import java.util.Optional; -public final class PostExecutionControl { +import io.fabric8.kubernetes.client.CustomResource; + +public final class PostExecutionControl> { private final boolean onlyFinalizerHandled; - private final CustomResource updatedCustomResource; + private final R updatedCustomResource; private final RuntimeException runtimeException; private PostExecutionControl( boolean onlyFinalizerHandled, - CustomResource updatedCustomResource, + R updatedCustomResource, RuntimeException runtimeException) { this.onlyFinalizerHandled = onlyFinalizerHandled; this.updatedCustomResource = updatedCustomResource; @@ -28,8 +29,9 @@ public static PostExecutionControl defaultDispatch() { return new PostExecutionControl(false, null, null); } - public static PostExecutionControl customResourceUpdated(CustomResource updatedCustomResource) { - return new PostExecutionControl(false, updatedCustomResource, null); + public static > PostExecutionControl customResourceUpdated( + R updatedCustomResource) { + return new PostExecutionControl<>(false, updatedCustomResource, null); } public static PostExecutionControl exceptionDuringExecution(RuntimeException exception) { @@ -40,7 +42,7 @@ public boolean isOnlyFinalizerHandled() { return onlyFinalizerHandled; } - public Optional getUpdatedCustomResource() { + public Optional getUpdatedCustomResource() { return Optional.ofNullable(updatedCustomResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 961a4c6c1f..99b85d6453 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -46,9 +46,8 @@ public class DefaultEventSourceManager> public DefaultEventSourceManager(ConfiguredController controller) { this(new DefaultEventHandler<>(controller), true); - registerEventSource( - CUSTOM_RESOURCE_EVENT_SOURCE_NAME, - new CustomResourceEventSource<>(controller.getCRClient(), controller.getConfiguration())); + registerEventSource(CUSTOM_RESOURCE_EVENT_SOURCE_NAME, + new CustomResourceEventSource<>(controller)); } @Override From 3428ca6c76835148cd50a651386e379093f61824 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 22:46:16 +0200 Subject: [PATCH 0017/1608] chore: clean up unused code --- .../CustomResourceSelectorTest.java | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java index 72c681b857..e87f344b85 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java @@ -10,24 +10,22 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.fabric8.kubernetes.client.Watcher; +import java.util.Objects; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import java.util.Objects; -import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; class CustomResourceSelectorTest { - public static final int FAKE_CONTROLLER_EXECUTION_DURATION = 250; public static final int SEPARATE_EXECUTION_TIMEOUT = 450; private final EventDispatcher eventDispatcherMock = mock(EventDispatcher.class); @@ -36,7 +34,6 @@ class CustomResourceSelectorTest { private final DefaultEventSourceManager defaultEventSourceManagerMock = mock(DefaultEventSourceManager.class); - private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private ControllerConfiguration configuration = mock(ControllerConfiguration.class); private final ConfigurationService configService = mock(ConfigurationService.class); @@ -125,13 +122,4 @@ private void waitMinimalTime() { } } - private CustomResourceEvent prepareCREvent() { - return prepareCREvent(UUID.randomUUID().toString()); - } - - private CustomResourceEvent prepareCREvent(String uid) { - TestCustomResource customResource = testCustomResource(uid); - customResourceCache.cacheResource(customResource); - return new CustomResourceEvent(Watcher.Action.MODIFIED, customResource, null); - } } From edfc4f83dbe97ce062f316611a6ed1d0bb23062c Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 27 Aug 2021 00:01:33 +0200 Subject: [PATCH 0018/1608] chore: use resources method instead of deprecated customResources --- .../io/javaoperatorsdk/operator/IntegrationTestSupport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java index bbdb510d8e..7dcee07b73 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java @@ -52,7 +52,7 @@ public void initialize(KubernetesClient k8sClient, ResourceController controller loadCRDAndApplyToCluster(crdPath); final var customResourceClass = config.getCustomResourceClass(); - this.crOperations = k8sClient.customResources(customResourceClass); + this.crOperations = k8sClient.resources(customResourceClass); final var namespaces = k8sClient.namespaces(); if (namespaces.withName(TEST_NAMESPACE).get() == null) { From 299c0b0aff0f6ccdad3eec2713d1b8e87f1285f7 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 27 Aug 2021 00:05:17 +0200 Subject: [PATCH 0019/1608] feat: add EventMonitor to decouple metrics from DefaultEventHandler A global EventMonitor is set when the Operator is created based on the Metrics instance provided by the ConfigurationService. --- .../io/javaoperatorsdk/operator/Operator.java | 23 +++++++---- .../processing/DefaultEventHandler.java | 40 +++++++++++-------- .../operator/processing/ExecutionScope.java | 5 ++- .../event/DefaultEventSourceManager.java | 4 ++ .../CustomResourceSelectorTest.java | 12 +----- .../processing/DefaultEventHandlerTest.java | 36 ++++++----------- .../operator/IntegrationTestSupport.java | 14 ++++--- 7 files changed, 68 insertions(+), 66 deletions(-) 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 0480b61fc0..680b59e8d4 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 @@ -15,6 +15,9 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.DefaultEventHandler; +import io.javaoperatorsdk.operator.processing.DefaultEventHandler.EventMonitor; +import io.javaoperatorsdk.operator.processing.event.Event; @SuppressWarnings("rawtypes") public class Operator implements AutoCloseable { @@ -24,16 +27,24 @@ public class Operator implements AutoCloseable { private final Object lock; private final List controllers; private volatile boolean started; - private final Metrics metrics; - public Operator( - KubernetesClient k8sClient, ConfigurationService configurationService, Metrics metrics) { + public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) { this.k8sClient = k8sClient; this.configurationService = configurationService; this.lock = new Object(); this.controllers = new ArrayList<>(); this.started = false; - this.metrics = metrics; + DefaultEventHandler.setEventMonitor(new EventMonitor() { + @Override + public void processedEvent(String uid, Event event) { + configurationService.getMetrics().timeControllerEvents(); + } + + @Override + public void failedEvent(String uid, Event event) { + configurationService.getMetrics().timeControllerRetry(); + } + }); } /** Adds a shutdown hook that automatically calls {@link #close()} when the app shuts down. */ @@ -41,10 +52,6 @@ public void installShutdownHook() { Runtime.getRuntime().addShutdownHook(new Thread(this::close)); } - public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) { - this(k8sClient, configurationService, Metrics.NOOP); - } - public KubernetesClient getKubernetesClient() { return k8sClient; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 312d35cae8..7be1ce5ed5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -20,7 +20,6 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -35,6 +34,13 @@ public class DefaultEventHandler> implements EventHandler { private static final Logger log = LoggerFactory.getLogger(DefaultEventHandler.class); + private static EventMonitor monitor = new EventMonitor() { + @Override + public void processedEvent(String uid, Event event) {} + + @Override + public void failedEvent(String uid, Event event) {} + }; private final EventBuffer eventBuffer; private final Set underProcessing = new HashSet<>(); @@ -46,7 +52,6 @@ public class DefaultEventHandler> implements Even private final int terminationTimeout; private final ReentrantLock lock = new ReentrantLock(); private DefaultEventSourceManager eventSourceManager; - private final ControllerConfiguration configuration; public DefaultEventHandler(ConfiguredController controller) { this( @@ -54,21 +59,20 @@ public DefaultEventHandler(ConfiguredController controller) { controller.getConfiguration().getName(), GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), controller.getConfiguration().getConfigurationService().concurrentReconciliationThreads(), - controller.getConfiguration().getConfigurationService().getTerminationTimeoutSeconds(), - controller.getConfiguration()); + controller.getConfiguration().getConfigurationService().getTerminationTimeoutSeconds()); } DefaultEventHandler( EventDispatcher eventDispatcher, String relatedControllerName, Retry retry, - int concurrentReconciliationThreads, ControllerConfiguration configuration) { + int concurrentReconciliationThreads) { this( eventDispatcher, relatedControllerName, retry, concurrentReconciliationThreads, - ConfigurationService.DEFAULT_TERMINATION_TIMEOUT_SECONDS, configuration); + ConfigurationService.DEFAULT_TERMINATION_TIMEOUT_SECONDS); } private DefaultEventHandler( @@ -76,7 +80,7 @@ private DefaultEventHandler( String relatedControllerName, Retry retry, int concurrentReconciliationThreads, - int terminationTimeout, ControllerConfiguration configuration) { + int terminationTimeout) { this.eventDispatcher = eventDispatcher; this.retry = retry; this.controllerName = relatedControllerName; @@ -86,7 +90,6 @@ private DefaultEventHandler( new ScheduledThreadPoolExecutor( concurrentReconciliationThreads, runnable -> new Thread(runnable, "EventHandler-" + relatedControllerName)); - this.configuration = configuration; } @Override @@ -106,6 +109,16 @@ public void setEventSourceManager(DefaultEventSourceManager eventSourceManage this.eventSourceManager = eventSourceManager; } + public static void setEventMonitor(EventMonitor monitor) { + DefaultEventHandler.monitor = monitor; + } + + public interface EventMonitor { + void processedEvent(String uid, Event event); + + void failedEvent(String uid, Event event); + } + @Override public void handleEvent(Event event) { try { @@ -115,10 +128,7 @@ public void handleEvent(Event event) { final Predicate selector = event.getCustomResourcesSelector(); for (String uid : eventSourceManager.getLatestResourceUids(selector)) { eventBuffer.addEvent(uid, event); - configuration - .getConfigurationService() - .getMetrics() - .timeControllerEvents(); + monitor.processedEvent(uid, event); executeBufferedEvents(uid); } } finally { @@ -168,10 +178,8 @@ void eventProcessingFinished( if (retry != null && postExecutionControl.exceptionDuringExecution()) { handleRetryOnException(executionScope); - configuration - .getConfigurationService() - .getMetrics() - .timeControllerRetry(); + executionScope.getEvents() + .forEach(e -> monitor.failedEvent(executionScope.getCustomResourceUid(), e)); return; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java index bb2ad813e3..53917cc094 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java @@ -1,11 +1,12 @@ package io.javaoperatorsdk.operator.processing; +import java.util.List; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.processing.event.Event; -import java.util.List; -public class ExecutionScope { +public class ExecutionScope> { private final List events; // the latest custom resource from cache diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 99b85d6453..8d9e89acfe 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -50,6 +50,10 @@ public DefaultEventSourceManager(ConfiguredController controller) { new CustomResourceEventSource<>(controller)); } + public DefaultEventHandler getEventHandler() { + return defaultEventHandler; + } + @Override public void close() { try { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java index e87f344b85..bef5b9fced 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java @@ -17,9 +17,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -34,16 +32,12 @@ class CustomResourceSelectorTest { private final DefaultEventSourceManager defaultEventSourceManagerMock = mock(DefaultEventSourceManager.class); - private ControllerConfiguration configuration = - mock(ControllerConfiguration.class); - private final ConfigurationService configService = mock(ConfigurationService.class); - private final DefaultEventHandler defaultEventHandler = new DefaultEventHandler( eventDispatcherMock, "Test", null, - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER, configuration); + ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); @BeforeEach public void setup() { @@ -63,10 +57,6 @@ public void setup() { }) .when(defaultEventSourceManagerMock) .cleanup(any()); - - when(configuration.getName()).thenReturn("DefaultEventHandlerTest"); - when(configService.getMetrics()).thenReturn(Metrics.NOOP); - when(configuration.getConfigurationService()).thenReturn(configService); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 699784180f..18fe3875f1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -13,21 +13,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.Watcher; -import io.javaoperatorsdk.operator.Metrics; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; -import io.javaoperatorsdk.operator.processing.retry.GenericRetry; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import java.util.Arrays; import java.util.List; import java.util.UUID; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -35,6 +24,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.client.Watcher; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; +import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + class DefaultEventHandlerTest { private static final Logger log = LoggerFactory.getLogger(DefaultEventHandlerTest.class); @@ -47,25 +46,20 @@ class DefaultEventHandlerTest { mock(DefaultEventSourceManager.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); - private ControllerConfiguration configuration = - mock(ControllerConfiguration.class); - private final ConfigurationService configService = mock(ConfigurationService.class); private DefaultEventHandler defaultEventHandler = new DefaultEventHandler( eventDispatcherMock, "Test", null, - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER, - configuration); + ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); private DefaultEventHandler defaultEventHandlerWithRetry = new DefaultEventHandler( eventDispatcherMock, "Test", GenericRetry.defaultLimitedExponentialRetry(), - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER, - configuration); + ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); @BeforeEach public void setup() { @@ -74,10 +68,6 @@ public void setup() { defaultEventHandler.setEventSourceManager(defaultEventSourceManagerMock); defaultEventHandlerWithRetry.setEventSourceManager(defaultEventSourceManagerMock); - when(configuration.getName()).thenReturn("DefaultEventHandlerTest"); - when(configService.getMetrics()).thenReturn(Metrics.NOOP); - when(configuration.getConfigurationService()).thenReturn(configService); - // todo: remove when(defaultEventSourceManagerMock.getCache()).thenReturn(customResourceCache); doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResource(any()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java index 7dcee07b73..271c50514b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java @@ -3,6 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.Namespace; import io.fabric8.kubernetes.api.model.NamespaceBuilder; @@ -19,11 +26,6 @@ import io.javaoperatorsdk.operator.processing.retry.Retry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceSpec; -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class IntegrationTestSupport { @@ -59,7 +61,7 @@ public void initialize(KubernetesClient k8sClient, ResourceController controller namespaces.create( new NamespaceBuilder().withNewMetadata().withName(TEST_NAMESPACE).endMetadata().build()); } - operator = new Operator(k8sClient, configurationService, Metrics.NOOP); + operator = new Operator(k8sClient, configurationService); final var overriddenConfig = ControllerConfigurationOverrider.override(config).settingNamespace(TEST_NAMESPACE); if (retry != null) { From 2916b28276de5aa37031fd8c6fd1a92430f67b62 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 26 Aug 2021 23:24:20 +0200 Subject: [PATCH 0020/1608] fix: use proper close method signature This is meant to avoid diamond inheritance issues where we could have conflicting version of the method if a class implements several interfaces with a close method. --- .../processing/event/DefaultEventSourceManager.java | 7 ++++++- .../operator/processing/event/EventHandler.java | 3 ++- .../operator/processing/event/EventSource.java | 3 ++- .../operator/processing/event/EventSourceManager.java | 6 ++++-- .../event/internal/CustomResourceEventSource.java | 3 ++- .../processing/event/DefaultEventSourceManagerTest.java | 9 ++++++--- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 8d9e89acfe..47542eb96d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; @@ -110,7 +111,11 @@ public Optional deRegisterEventSource(String name) { lock.lock(); EventSource currentEventSource = eventSources.remove(name); if (currentEventSource != null) { - currentEventSource.close(); + try { + currentEventSource.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } } return Optional.ofNullable(currentEventSource); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventHandler.java index d09a1c6d31..e0a657e1d1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventHandler.java @@ -1,11 +1,12 @@ package io.javaoperatorsdk.operator.processing.event; import java.io.Closeable; +import java.io.IOException; public interface EventHandler extends Closeable { void handleEvent(Event event); @Override - default void close() {} + default void close() throws IOException {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java index dd695ea439..149e0acc1c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; import java.io.Closeable; +import java.io.IOException; public interface EventSource extends Closeable { @@ -15,7 +16,7 @@ default void start() {} * {@link EventSourceManager}. */ @Override - default void close() {} + default void close() throws IOException {} void setEventHandler(EventHandler eventHandler); 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 241bad90b2..b59491042b 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,10 +1,12 @@ package io.javaoperatorsdk.operator.processing.event; -import io.javaoperatorsdk.operator.OperatorException; import java.io.Closeable; +import java.io.IOException; import java.util.Map; import java.util.Optional; +import io.javaoperatorsdk.operator.OperatorException; + public interface EventSourceManager extends Closeable { /** @@ -35,5 +37,5 @@ Optional deRegisterCustomResourceFromEventSource( Map getRegisteredEventSources(); @Override - default void close() {} + default void close() throws IOException {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index a6e532c73b..05960e71f0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -4,6 +4,7 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; +import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -68,7 +69,7 @@ public void start() { } @Override - public void close() { + public void close() throws IOException { eventHandler.close(); for (Watch watch : this.watches) { try { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java index 42fa9fa95d..f8f58e0aed 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java @@ -8,12 +8,15 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.io.IOException; +import java.util.Map; + +import org.junit.jupiter.api.Test; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.DefaultEventHandler; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; -import java.util.Map; -import org.junit.jupiter.api.Test; class DefaultEventSourceManagerTest { @@ -38,7 +41,7 @@ public void registersEventSource() { } @Test - public void closeShouldCascadeToEventSources() { + public void closeShouldCascadeToEventSources() throws IOException { EventSource eventSource = mock(EventSource.class); EventSource eventSource2 = mock(EventSource.class); defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, eventSource); From 61d1f8ac29d00398ddd0c2eef8303250d747042b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 27 Aug 2021 00:23:25 +0200 Subject: [PATCH 0021/1608] refactor: use more appropriate method names --- .../io/javaoperatorsdk/operator/Metrics.java | 30 ++++++++++++++----- .../io/javaoperatorsdk/operator/Operator.java | 4 +-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java index 6edc599e47..0a920b3a4d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java @@ -1,18 +1,34 @@ package io.javaoperatorsdk.operator; +import java.util.concurrent.TimeUnit; +import java.util.function.ToDoubleFunction; +import java.util.function.ToLongFunction; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.DeleteControl; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.FunctionTimer; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Measurement; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.instrument.distribution.pause.PauseDetector; -import io.micrometer.core.instrument.noop.*; -import java.util.concurrent.TimeUnit; -import java.util.function.ToDoubleFunction; -import java.util.function.ToLongFunction; +import io.micrometer.core.instrument.noop.NoopCounter; +import io.micrometer.core.instrument.noop.NoopDistributionSummary; +import io.micrometer.core.instrument.noop.NoopFunctionCounter; +import io.micrometer.core.instrument.noop.NoopFunctionTimer; +import io.micrometer.core.instrument.noop.NoopGauge; +import io.micrometer.core.instrument.noop.NoopMeter; +import io.micrometer.core.instrument.noop.NoopTimer; public class Metrics { public static final Metrics NOOP = new Metrics(new NoopMeterRegistry(Clock.SYSTEM)); @@ -97,7 +113,7 @@ public DeleteControl timeControllerDelete( } } - public void timeControllerRetry() { + public void incrementControllerRetriesNumber() { registry .counter( @@ -107,7 +123,7 @@ public void timeControllerRetry() { } - public void timeControllerEvents() { + public void incrementProcessedEventsNumber() { registry .counter( 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 680b59e8d4..012dbd8b98 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 @@ -37,12 +37,12 @@ public Operator(KubernetesClient k8sClient, ConfigurationService configurationSe DefaultEventHandler.setEventMonitor(new EventMonitor() { @Override public void processedEvent(String uid, Event event) { - configurationService.getMetrics().timeControllerEvents(); + configurationService.getMetrics().incrementProcessedEventsNumber(); } @Override public void failedEvent(String uid, Event event) { - configurationService.getMetrics().timeControllerRetry(); + configurationService.getMetrics().incrementControllerRetriesNumber(); } }); } From adca825481fb7d4a05fbaa3c6b2724cd058784e6 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 27 Aug 2021 00:40:27 +0200 Subject: [PATCH 0022/1608] refactor: simplify test-specific constructor --- .../operator/processing/DefaultEventHandler.java | 10 +++------- .../processing/CustomResourceSelectorTest.java | 7 +------ .../processing/DefaultEventHandlerTest.java | 14 +++----------- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 7be1ce5ed5..4c33ec23b1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -62,16 +62,12 @@ public DefaultEventHandler(ConfiguredController controller) { controller.getConfiguration().getConfigurationService().getTerminationTimeoutSeconds()); } - DefaultEventHandler( - EventDispatcher eventDispatcher, - String relatedControllerName, - Retry retry, - int concurrentReconciliationThreads) { + DefaultEventHandler(EventDispatcher dispatcher, String relatedControllerName, Retry retry) { this( - eventDispatcher, + dispatcher, relatedControllerName, retry, - concurrentReconciliationThreads, + ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER, ConfigurationService.DEFAULT_TERMINATION_TIMEOUT_SECONDS); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java index bef5b9fced..695a985ce0 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -33,11 +32,7 @@ class CustomResourceSelectorTest { mock(DefaultEventSourceManager.class); private final DefaultEventHandler defaultEventHandler = - new DefaultEventHandler( - eventDispatcherMock, - "Test", - null, - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); + new DefaultEventHandler(eventDispatcherMock, "Test", null); @BeforeEach public void setup() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 18fe3875f1..bba02b14fd 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -25,7 +25,6 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.Watcher; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; @@ -48,18 +47,11 @@ class DefaultEventHandlerTest { private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private DefaultEventHandler defaultEventHandler = - new DefaultEventHandler( - eventDispatcherMock, - "Test", - null, - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); + new DefaultEventHandler(eventDispatcherMock, "Test", null); private DefaultEventHandler defaultEventHandlerWithRetry = - new DefaultEventHandler( - eventDispatcherMock, - "Test", - GenericRetry.defaultLimitedExponentialRetry(), - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER); + new DefaultEventHandler(eventDispatcherMock, "Test", + GenericRetry.defaultLimitedExponentialRetry()); @BeforeEach public void setup() { From fc300c396fa9f2578e579b11ff99ccc0a7732558 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 27 Aug 2021 12:09:56 +0200 Subject: [PATCH 0023/1608] refactor: use ReentrantLock instead of locking on Object --- .../io/javaoperatorsdk/operator/Operator.java | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) 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 012dbd8b98..85279d946d 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 @@ -1,9 +1,9 @@ package io.javaoperatorsdk.operator; -import java.io.Closeable; import java.io.IOException; -import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; +import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,16 +24,13 @@ public class Operator implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient k8sClient; private final ConfigurationService configurationService; - private final Object lock; - private final List controllers; - private volatile boolean started; + private final ReentrantLock lock = new ReentrantLock(); + private final List controllers = new LinkedList<>(); + private volatile boolean started = false; public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) { this.k8sClient = k8sClient; this.configurationService = configurationService; - this.lock = new Object(); - this.controllers = new ArrayList<>(); - this.started = false; DefaultEventHandler.setEventMonitor(new EventMonitor() { @Override public void processedEvent(String uid, Event event) { @@ -67,10 +64,14 @@ public ConfigurationService getConfigurationService() { */ @SuppressWarnings("unchecked") public void start() { - synchronized (lock) { + try { + lock.lock(); if (started) { return; } + if (controllers.isEmpty()) { + throw new OperatorException("No ResourceController exists. Exiting!"); + } final var version = configurationService.getVersion(); log.info( @@ -79,10 +80,6 @@ public void start() { version.getCommit(), version.getBuiltTime()); - if (controllers.isEmpty()) { - throw new OperatorException("No ResourceController exists. Exiting!"); - } - log.info("Client version: {}", Version.clientVersion()); try { final var k8sVersion = k8sClient.getVersion(); @@ -94,33 +91,37 @@ public void start() { throw new OperatorException("Error retrieving the server version", e); } - controllers.forEach(ConfiguredController::start); - + controllers.parallelStream().forEach(ConfiguredController::start); started = true; + } finally { + lock.unlock(); } } /** Stop the operator. */ @Override public void close() { - synchronized (lock) { + log.info( + "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); + + try { + lock.lock(); if (!started) { return; } - log.info( - "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); - - for (Closeable closeable : this.controllers) { + this.controllers.parallelStream().forEach(closeable -> { try { log.debug("closing {}", closeable); closeable.close(); } catch (IOException e) { log.warn("Error closing {}", closeable, e); } - } + }); started = false; + } finally { + lock.unlock(); } } From 28eff31ae113ed367b16c2d4f9a315023f4c5498 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 27 Aug 2021 12:32:22 +0200 Subject: [PATCH 0024/1608] feat: improve error logging when cluster is not online --- .../java/io/javaoperatorsdk/operator/Operator.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 85279d946d..054d7ff6fa 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 @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator; import java.io.IOException; +import java.net.ConnectException; import java.util.LinkedList; import java.util.List; import java.util.concurrent.locks.ReentrantLock; @@ -87,8 +88,14 @@ public void start() { log.info("Server version: {}.{}", k8sVersion.getMajor(), k8sVersion.getMinor()); } } catch (Exception e) { - log.error("Error retrieving the server version. Exiting!", e); - throw new OperatorException("Error retrieving the server version", e); + final String error; + if (e.getCause() instanceof ConnectException) { + error = "Cannot connect to cluster"; + } else { + error = "Error retrieving the server version"; + } + log.error(error, e); + throw new OperatorException(error, e); } controllers.parallelStream().forEach(ConfiguredController::start); From f229802e508ee6030255bacf138e83c4c9429949 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 27 Aug 2021 13:14:24 +0200 Subject: [PATCH 0025/1608] refactor: generify timing of controller execution Introduce ControllerExecution interface to encapsulate a controller call that can be timed. Move logic of call to ConfiguredController where it's more logical so that Metrics class doesn't have any business logic anymore. --- .../io/javaoperatorsdk/operator/Metrics.java | 80 ++++--------------- .../processing/ConfiguredController.java | 61 +++++++++++++- .../operator/processing/EventDispatcher.java | 12 +-- 3 files changed, 78 insertions(+), 75 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java index 0a920b3a4d..0dd021b51f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java @@ -4,12 +4,6 @@ import java.util.function.ToDoubleFunction; import java.util.function.ToLongFunction; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.DistributionSummary; @@ -38,76 +32,36 @@ public Metrics(MeterRegistry registry) { this.registry = registry; } - public UpdateControl timeControllerCreateOrUpdate( - ResourceController controller, - ControllerConfiguration configuration, - R resource, - Context context) { - final var name = configuration.getName(); - final var timer = - Timer.builder("operator.sdk.controllers.execution.createorupdate") - .tags("controller", name) - .publishPercentiles(0.3, 0.5, 0.95) - .publishPercentileHistogram() - .register(registry); - try { - final var result = timer.record(() -> controller.createOrUpdateResource(resource, context)); - String successType = "cr"; - if (result.isUpdateStatusSubResource()) { - successType = "status"; - } - if (result.isUpdateCustomResourceAndStatusSubResource()) { - successType = "both"; - } - registry - .counter( - "operator.sdk.controllers.execution.success", "controller", name, "type", successType) - .increment(); - return result; - } catch (Exception e) { - registry - .counter( - "operator.sdk.controllers.execution.failure", - "controller", - name, - "exception", - e.getClass().getSimpleName()) - .increment(); - throw e; - } + public interface ControllerExecution { + String name(); + + String controllerName(); + + String successTypeName(T result); + + T execute(); } - public DeleteControl timeControllerDelete( - ResourceController controller, - ControllerConfiguration configuration, - CustomResource resource, - Context context) { - final var name = configuration.getName(); + public T timeControllerExecution(ControllerExecution execution) { + final var name = execution.controllerName(); + final var execName = "operator.sdk.controllers.execution." + execution.name(); final var timer = - Timer.builder("operator.sdk.controllers.execution.delete") + Timer.builder(execName) .tags("controller", name) .publishPercentiles(0.3, 0.5, 0.95) .publishPercentileHistogram() .register(registry); try { - final var result = timer.record(() -> controller.deleteResource(resource, context)); - String successType = "notDelete"; - if (result == DeleteControl.DEFAULT_DELETE) { - successType = "delete"; - } + final var result = timer.record(execution::execute); + final var successType = execution.successTypeName(result); registry - .counter( - "operator.sdk.controllers.execution.success", "controller", name, "type", successType) + .counter(execName + ".success", "controller", name, "type", successType) .increment(); return result; } catch (Exception e) { + final var exception = e.getClass().getSimpleName(); registry - .counter( - "operator.sdk.controllers.execution.failure", - "controller", - name, - "exception", - e.getClass().getSimpleName()) + .counter(execName + ".failure", "controller", name, "exception", exception) .increment(); throw e; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index 7f75cc4920..4ea4f34baa 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -11,6 +11,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.CustomResourceUtils; +import io.javaoperatorsdk.operator.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.Context; @@ -38,12 +39,68 @@ public ConfiguredController(ResourceController controller, @Override public DeleteControl deleteResource(R resource, Context context) { - return controller.deleteResource(resource, context); + return configuration.getConfigurationService().getMetrics().timeControllerExecution( + new ControllerExecution<>() { + @Override + public String name() { + return "delete"; + } + + @Override + public String controllerName() { + return configuration.getName(); + } + + @Override + public String successTypeName(DeleteControl result) { + switch (result) { + case DEFAULT_DELETE: + return "delete"; + case NO_FINALIZER_REMOVAL: + return "finalizerNotRemoved"; + default: + return "unknown"; + } + } + + @Override + public DeleteControl execute() { + return controller.deleteResource(resource, context); + } + }); } @Override public UpdateControl createOrUpdateResource(R resource, Context context) { - return controller.createOrUpdateResource(resource, context); + return configuration.getConfigurationService().getMetrics().timeControllerExecution( + new ControllerExecution<>() { + @Override + public String name() { + return "createOrUpdate"; + } + + @Override + public String controllerName() { + return configuration.getName(); + } + + @Override + public String successTypeName(UpdateControl result) { + String successType = "cr"; + if (result.isUpdateStatusSubResource()) { + successType = "status"; + } + if (result.isUpdateCustomResourceAndStatusSubResource()) { + successType = "both"; + } + return successType; + } + + @Override + public UpdateControl execute() { + return controller.createOrUpdateResource(resource, context); + } + }); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 58e1a3fdd7..fec4c14943 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -124,11 +124,7 @@ private PostExecutionControl handleCreateOrUpdate( getVersion(resource), executionScope); - UpdateControl updateControl = - configuration() - .getConfigurationService() - .getMetrics() - .timeControllerCreateOrUpdate(controller, configuration(), resource, context); + UpdateControl updateControl = controller.createOrUpdateResource(resource, context); R updatedCustomResource = null; if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); @@ -159,11 +155,7 @@ private PostExecutionControl handleDelete(R resource, Context context) { getName(resource), getVersion(resource)); - DeleteControl deleteControl = - configuration() - .getConfigurationService() - .getMetrics() - .timeControllerDelete(controller, configuration(), resource, context); + DeleteControl deleteControl = controller.deleteResource(resource, context); final var useFinalizer = configuration().useFinalizer(); if (useFinalizer) { if (deleteControl == DeleteControl.DEFAULT_DELETE From 0831be1622f273dc70e4ec1461cd483353e3c229 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 30 Aug 2021 09:56:24 +0200 Subject: [PATCH 0026/1608] chore: remove synchronization altogether --- .../io/javaoperatorsdk/operator/Operator.java | 105 ++++++++---------- 1 file changed, 46 insertions(+), 59 deletions(-) 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 054d7ff6fa..b536d3a9f5 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 @@ -25,7 +25,6 @@ public class Operator implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient k8sClient; private final ConfigurationService configurationService; - private final ReentrantLock lock = new ReentrantLock(); private final List controllers = new LinkedList<>(); private volatile boolean started = false; @@ -65,44 +64,39 @@ public ConfigurationService getConfigurationService() { */ @SuppressWarnings("unchecked") public void start() { - try { - lock.lock(); - if (started) { - return; - } - if (controllers.isEmpty()) { - throw new OperatorException("No ResourceController exists. Exiting!"); - } + if (started) { + return; + } + if (controllers.isEmpty()) { + throw new OperatorException("No ResourceController exists. Exiting!"); + } - final var version = configurationService.getVersion(); - log.info( - "Operator SDK {} (commit: {}) built on {} starting...", - version.getSdkVersion(), - version.getCommit(), - version.getBuiltTime()); + final var version = configurationService.getVersion(); + log.info( + "Operator SDK {} (commit: {}) built on {} starting...", + version.getSdkVersion(), + version.getCommit(), + version.getBuiltTime()); - log.info("Client version: {}", Version.clientVersion()); - try { - final var k8sVersion = k8sClient.getVersion(); - if (k8sVersion != null) { - log.info("Server version: {}.{}", k8sVersion.getMajor(), k8sVersion.getMinor()); - } - } catch (Exception e) { - final String error; - if (e.getCause() instanceof ConnectException) { - error = "Cannot connect to cluster"; - } else { - error = "Error retrieving the server version"; - } - log.error(error, e); - throw new OperatorException(error, e); + log.info("Client version: {}", Version.clientVersion()); + try { + final var k8sVersion = k8sClient.getVersion(); + if (k8sVersion != null) { + log.info("Server version: {}.{}", k8sVersion.getMajor(), k8sVersion.getMinor()); } - - controllers.parallelStream().forEach(ConfiguredController::start); - started = true; - } finally { - lock.unlock(); + } catch (Exception e) { + final String error; + if (e.getCause() instanceof ConnectException) { + error = "Cannot connect to cluster"; + } else { + error = "Error retrieving the server version"; + } + log.error(error, e); + throw new OperatorException(error, e); } + + controllers.parallelStream().forEach(ConfiguredController::start); + started = true; } /** Stop the operator. */ @@ -111,25 +105,20 @@ public void close() { log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); - try { - lock.lock(); - if (!started) { - return; + if (!started) { + return; + } + + this.controllers.parallelStream().forEach(closeable -> { + try { + log.debug("closing {}", closeable); + closeable.close(); + } catch (IOException e) { + log.warn("Error closing {}", closeable, e); } + }); - this.controllers.parallelStream().forEach(closeable -> { - try { - log.debug("closing {}", closeable); - closeable.close(); - } catch (IOException e) { - log.warn("Error closing {}", closeable, e); - } - }); - - started = false; - } finally { - lock.unlock(); - } + started = false; } /** @@ -173,13 +162,11 @@ public void register( if (configuration == null) { configuration = existing; } - synchronized (lock) { - final var configuredController = - new ConfiguredController(controller, configuration, k8sClient); - this.controllers.add(configuredController); - if (started) { - configuredController.start(); - } + final var configuredController = + new ConfiguredController(controller, configuration, k8sClient); + this.controllers.add(configuredController); + if (started) { + configuredController.start(); } final var watchedNS = From 19d70f2a61bae39f9b2e69b6cf3b4e8586cc0092 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 30 Aug 2021 20:58:08 +0200 Subject: [PATCH 0027/1608] fix: avoid NPE in ConfiguredController.close if manager is not set Note that this shouldn't happen during normal execution but can happen during tests. --- .../operator/processing/ConfiguredController.java | 4 +++- .../event/internal/CustomResourceEventSourceTest.java | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index 4ea4f34baa..31b06dd7af 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -221,6 +221,8 @@ private boolean failOnMissingCurrentNS() { @Override public void close() throws IOException { - manager.close(); + if (manager != null) { + manager.close(); + } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 08f4174023..bcf30501d4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -118,6 +118,7 @@ private static class TestConfiguration implements final ConfigurationService service = mock(ConfigurationService.class); final boolean generationAware; + public TestConfiguration(boolean generationAware) { when(service.getObjectMapper()).thenReturn(ConfigurationService.OBJECT_MAPPER); this.generationAware = generationAware; From 174472ec19ffadea95e47598b9459d508b64ce87 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 30 Aug 2021 21:22:01 +0200 Subject: [PATCH 0028/1608] fix: improper TestConfiguration implementation --- .../CustomResourceEventSourceTest.java | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index bcf30501d4..072e6cc986 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -6,11 +6,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import java.time.LocalDateTime; -import java.util.Arrays; - import java.util.List; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,6 +17,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.api.config.AbstractControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -113,30 +112,15 @@ public MixedOperation { + private static class TestConfiguration extends + AbstractControllerConfiguration { final ConfigurationService service = mock(ConfigurationService.class); - final boolean generationAware; public TestConfiguration(boolean generationAware) { + super(null, null, null, FINALIZER, generationAware, null, null, null); when(service.getObjectMapper()).thenReturn(ConfigurationService.OBJECT_MAPPER); - this.generationAware = generationAware; - } - - @Override - public String getAssociatedControllerClassName() { - return null; - } - - @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public boolean isGenerationAware() { - return generationAware; + setConfigurationService(service); } } } From 8ffddfc6f8c8678602ba1bd6a1c594b6178033df Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 31 Aug 2021 11:15:52 +0200 Subject: [PATCH 0029/1608] feat: make AbstractControllerConfiguration directly usable Obviously, it should be renamed to something else since it's not abstract anymore but doing so would break backwards compatibility so best left for some later time. --- .../AbstractControllerConfiguration.java | 24 +++++++++++++++---- .../ControllerConfigurationOverrider.java | 14 +++-------- .../CustomResourceEventSourceTest.java | 10 ++++---- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java index 6db3d682d6..cc8a96fcf1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.client.CustomResource; -public abstract class AbstractControllerConfiguration> +public class AbstractControllerConfiguration> implements ControllerConfiguration { private final String associatedControllerClassName; @@ -17,6 +17,7 @@ public abstract class AbstractControllerConfiguration customResourceClass; private ConfigurationService service; public AbstractControllerConfiguration( @@ -27,7 +28,9 @@ public AbstractControllerConfiguration( boolean generationAware, Set namespaces, RetryConfiguration retryConfiguration, - String labelSelector) { + String labelSelector, + Class customResourceClass, + ConfigurationService service) { this.associatedControllerClassName = associatedControllerClassName; this.name = name; this.crdName = crdName; @@ -41,11 +44,15 @@ public AbstractControllerConfiguration( ? ControllerConfiguration.super.getRetryConfiguration() : retryConfiguration; this.labelSelector = labelSelector; + this.customResourceClass = + customResourceClass == null ? ControllerConfiguration.super.getCustomResourceClass() + : customResourceClass; + setConfigurationService(service); } /** * @deprecated use - * {@link #AbstractControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration, String)} + * {@link #AbstractControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration, String, Class, ConfigurationService)} * instead */ @Deprecated @@ -58,7 +65,7 @@ public AbstractControllerConfiguration( Set namespaces, RetryConfiguration retryConfiguration) { this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, - retryConfiguration, null); + retryConfiguration, null, null, null); } @Override @@ -108,6 +115,10 @@ public ConfigurationService getConfigurationService() { @Override public void setConfigurationService(ConfigurationService service) { + if (this.service != null) { + throw new RuntimeException("A ConfigurationService is already associated with '" + name + + "' ControllerConfiguration. Cannot change it once set!"); + } this.service = service; } @@ -115,4 +126,9 @@ public void setConfigurationService(ConfigurationService service) { public String getLabelSelector() { return labelSelector; } + + @Override + public Class getCustomResourceClass() { + return customResourceClass; + } } 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 7882ccf340..51ff31af00 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 @@ -74,17 +74,9 @@ public ControllerConfiguration build() { generationAware, namespaces, retry, - labelSelector) { - @Override - public Class getCustomResourceClass() { - return original.getCustomResourceClass(); - } - - @Override - public ConfigurationService getConfigurationService() { - return original.getConfigurationService(); - } - }; + labelSelector, + original.getCustomResourceClass(), + original.getConfigurationService()); } public static > ControllerConfigurationOverrider override( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 072e6cc986..1448982379 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -115,12 +115,12 @@ public MixedOperation { - final ConfigurationService service = mock(ConfigurationService.class); - public TestConfiguration(boolean generationAware) { - super(null, null, null, FINALIZER, generationAware, null, null, null); - when(service.getObjectMapper()).thenReturn(ConfigurationService.OBJECT_MAPPER); - setConfigurationService(service); + super(null, null, null, FINALIZER, generationAware, null, null, null, + TestCustomResource.class, + mock(ConfigurationService.class)); + when(getConfigurationService().getObjectMapper()) + .thenReturn(ConfigurationService.OBJECT_MAPPER); } } } From 29f16b198ae0d7442cbd7dc6dd5da2d919c508fc Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 31 Aug 2021 11:55:34 +0200 Subject: [PATCH 0030/1608] fix: add ControllerManager to handle controllers and status atomically --- .../io/javaoperatorsdk/operator/Operator.java | 80 ++++++++++++------- 1 file changed, 51 insertions(+), 29 deletions(-) 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 b536d3a9f5..c3a2360be4 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 @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator; +import java.io.Closeable; import java.io.IOException; import java.net.ConnectException; import java.util.LinkedList; @@ -25,8 +26,7 @@ public class Operator implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient k8sClient; private final ConfigurationService configurationService; - private final List controllers = new LinkedList<>(); - private volatile boolean started = false; + private final ControllerManager controllers = new ControllerManager(); public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) { this.k8sClient = k8sClient; @@ -62,14 +62,8 @@ public ConfigurationService getConfigurationService() { * where there is no obvious entrypoint to the application which can trigger the injection process * and start the cluster monitoring processes. */ - @SuppressWarnings("unchecked") public void start() { - if (started) { - return; - } - if (controllers.isEmpty()) { - throw new OperatorException("No ResourceController exists. Exiting!"); - } + controllers.shouldStart(); final var version = configurationService.getVersion(); log.info( @@ -95,8 +89,7 @@ public void start() { throw new OperatorException(error, e); } - controllers.parallelStream().forEach(ConfiguredController::start); - started = true; + controllers.start(); } /** Stop the operator. */ @@ -105,20 +98,7 @@ public void close() { log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); - if (!started) { - return; - } - - this.controllers.parallelStream().forEach(closeable -> { - try { - log.debug("closing {}", closeable); - closeable.close(); - } catch (IOException e) { - log.warn("Error closing {}", closeable, e); - } - }); - - started = false; + controllers.close(); } /** @@ -164,10 +144,7 @@ public void register( } final var configuredController = new ConfiguredController(controller, configuration, k8sClient); - this.controllers.add(configuredController); - if (started) { - configuredController.start(); - } + controllers.add(configuredController); final var watchedNS = configuration.watchAllNamespaces() @@ -180,4 +157,49 @@ public void register( watchedNS); } } + + private static class ControllerManager implements Closeable { + private final List controllers = new LinkedList<>(); + private boolean started = false; + + + public synchronized void shouldStart() { + if (started) { + return; + } + if (controllers.isEmpty()) { + throw new OperatorException("No ResourceController exists. Exiting!"); + } + } + + public synchronized void start() { + controllers.parallelStream().forEach(ConfiguredController::start); + started = true; + } + + @Override + public synchronized void close() { + if (!started) { + return; + } + + this.controllers.parallelStream().forEach(closeable -> { + try { + log.debug("closing {}", closeable); + closeable.close(); + } catch (IOException e) { + log.warn("Error closing {}", closeable, e); + } + }); + + started = false; + } + + public synchronized void add(ConfiguredController configuredController) { + this.controllers.add(configuredController); + if (started) { + configuredController.start(); + } + } + } } From aff192e0a322b1c6da833f6e4954641ec48e781e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 31 Aug 2021 12:17:46 +0200 Subject: [PATCH 0031/1608] fix: revert to use updateStatus for now It seems like using replaceStatus is causing issues with integration tests. patchStatus would work but this will be discussed in a subsequent issue / PR. --- .../io/javaoperatorsdk/operator/processing/EventDispatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index fec4c14943..8eef425dad 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -219,7 +219,7 @@ public R updateStatus(R resource) { return resourceOperation .inNamespace(resource.getMetadata().getNamespace()) .withName(getName(resource)) - .replaceStatus(resource); + .updateStatus(resource); } public R replaceWithLock(R resource) { From 4cbd08e4f63864f352fcb3c7753559e41a4e7f53 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 31 Aug 2021 18:11:05 +0200 Subject: [PATCH 0032/1608] chore(ci): use caching --- .github/workflows/master-snapshot-release.yml | 6 ++++-- .github/workflows/pr.yml | 3 ++- .github/workflows/release.yml | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index a3a87a8359..8ad8ea60bc 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -27,10 +27,11 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - name: Set up Java and Maven - uses: actions/setup-java@v2.3.0 + uses: actions/setup-java@v2 with: distribution: ${{ matrix.distribution }} java-version: ${{ matrix.java }} + cache: 'maven' - name: Run unit tests run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml - name: Set up Minikube @@ -47,10 +48,11 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Java and Maven - uses: actions/setup-java@v2.3.0 + uses: actions/setup-java@v2 with: distribution: temurin java-version: 11 + cache: 'maven' - name: Release Maven package uses: samuelmeuli/action-maven-publish@v1 with: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fd494a0f5e..f2ed4b6419 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -27,10 +27,11 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - name: Set up Java and Maven - uses: actions/setup-java@v2.3.0 + uses: actions/setup-java@v2 with: distribution: ${{ matrix.distribution }} java-version: ${{ matrix.java }} + cache: 'maven' - name: Check code format run: ./mvnw ${MAVEN_ARGS} formatter:validate -Dconfigfile=$PWD/contributing/eclipse-google-style.xml --file pom.xml - name: Run unit tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5424161349..4a8c53bd9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,10 +10,11 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Java and Maven - uses: actions/setup-java@v2.3.0 + uses: actions/setup-java@v2 with: java-version: 11 distribution: temurin + cache: 'maven' - name: change version to release version # Assume that RELEASE_VERSION will have form like: "v1.0.1". So we cut the "v" run: ./mvnw ${MAVEN_ARGS} versions:set -DnewVersion="${RELEASE_VERSION:1}" versions:commit @@ -34,10 +35,11 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Java and Maven - uses: actions/setup-java@v2.3.0 + uses: actions/setup-java@v2 with: java-version: 11 distribution: temurin + cache: 'maven' - name: change version to release version run: | ./mvnw ${MAVEN_ARGS} versions:set -DnewVersion="${RELEASE_VERSION:1}" versions:commit From 32a7c73e143d366833efbd1cf22284f610796bff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Sep 2021 03:15:02 +0000 Subject: [PATCH 0033/1608] chore(deps): bump jandex-maven-plugin from 1.1.1 to 1.2.0 Bumps [jandex-maven-plugin](https://github.com/wildfly/jandex-maven-plugin) from 1.1.1 to 1.2.0. - [Release notes](https://github.com/wildfly/jandex-maven-plugin/releases) - [Commits](https://github.com/wildfly/jandex-maven-plugin/compare/1.1.1...1.2.0) --- updated-dependencies: - dependency-name: org.jboss.jandex:jandex-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 183436f4c7..aed182e031 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 2.8.2 2.5.2 5.0.0 - 1.1.1 + 1.2.0 2.16.0 0.3.1 From 9341dcaac7bd9f87ce89ed660982bcb14d5b0482 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Sep 2021 03:15:10 +0000 Subject: [PATCH 0034/1608] chore(deps): bump kubernetes-client-bom from 5.7.0 to 5.7.1 Bumps [kubernetes-client-bom](https://github.com/fabric8io/kubernetes-client) from 5.7.0 to 5.7.1. - [Release notes](https://github.com/fabric8io/kubernetes-client/releases) - [Changelog](https://github.com/fabric8io/kubernetes-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/fabric8io/kubernetes-client/compare/v5.7.0...v5.7.1) --- updated-dependencies: - dependency-name: io.fabric8:kubernetes-client-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aed182e031..62cfd2f378 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ ${java.version} 5.7.2 - 5.7.0 + 5.7.1 1.7.32 2.14.1 3.12.4 From 8a9c614df3ec56bf884b01625db6dc82133d7fe3 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 2 Sep 2021 20:54:00 +0200 Subject: [PATCH 0035/1608] fix: jandex plugin 1.2 brings jandex 2.4.0 causing compatibility issues Newer Jandex versions generate indices that cannot be read by older versions. This is causing issues, in particular, with the currently released Quarkus version. Fabric8 5.7.1 was also bringing Jandex 2.4, which is reverted in 5.7.2. --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 62cfd2f378..a239ad361c 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ ${java.version} 5.7.2 - 5.7.1 + 5.7.2 1.7.32 2.14.1 3.12.4 @@ -67,7 +67,7 @@ 2.8.2 2.5.2 5.0.0 - 1.2.0 + 1.1.1 2.16.0 0.3.1 From 8d1b15eb471e5e60c19a653fdc57e5ae93c73bdb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 2 Sep 2021 19:07:24 +0000 Subject: [PATCH 0036/1608] Set new SNAPSHOT version into pom files. --- operator-framework-core/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- samples/common/pom.xml | 2 +- samples/pom.xml | 2 +- samples/pure-java/pom.xml | 2 +- samples/spring-boot-plain/pom.xml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 68213a7907..8d0515e545 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.6-SNAPSHOT + 1.9.7-SNAPSHOT ../pom.xml diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index f59686675b..1e0a3f826e 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.6-SNAPSHOT + 1.9.7-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index a239ad361c..526e1c4bb4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.6-SNAPSHOT + 1.9.7-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/samples/common/pom.xml b/samples/common/pom.xml index e2971ec8ff..303f349e64 100644 --- a/samples/common/pom.xml +++ b/samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.6-SNAPSHOT + 1.9.7-SNAPSHOT operator-framework-samples-common diff --git a/samples/pom.xml b/samples/pom.xml index 17944003e0..912026ad6a 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.6-SNAPSHOT + 1.9.7-SNAPSHOT java-operator-sdk-samples diff --git a/samples/pure-java/pom.xml b/samples/pure-java/pom.xml index e608a3439b..32d3f0adb4 100644 --- a/samples/pure-java/pom.xml +++ b/samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.6-SNAPSHOT + 1.9.7-SNAPSHOT operator-framework-samples-pure-java diff --git a/samples/spring-boot-plain/pom.xml b/samples/spring-boot-plain/pom.xml index 7131c8a7e0..0cf16191d0 100644 --- a/samples/spring-boot-plain/pom.xml +++ b/samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.6-SNAPSHOT + 1.9.7-SNAPSHOT operator-framework-samples-spring-boot-plain From d40c64ef3d4d61d80a54fac366644f7c512fca4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Sep 2021 09:31:48 +0200 Subject: [PATCH 0037/1608] chore(deps): bump maven-javadoc-plugin from 3.3.0 to 3.3.1 (#528) Bumps [maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.3.0 to 3.3.1. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.3.0...maven-javadoc-plugin-3.3.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 526e1c4bb4..5f7ce60df0 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 2.11 3.8.1 3.0.0-M5 - 3.3.0 + 3.3.1 3.2.0 3.2.1 3.2.0 From 235da3211f4d5543a9094e7c5223359f5551037c Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 7 Sep 2021 17:04:59 +0200 Subject: [PATCH 0038/1608] fix: add support for label selector --- .../operator/config/runtime/AnnotationConfiguration.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index dc03670ff6..194eed541a 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -50,6 +50,11 @@ public Set getNamespaces() { return Set.of(annotation.map(Controller::namespaces).orElse(new String[] {})); } + @Override + public String getLabelSelector() { + return annotation.map(Controller::labelSelector).orElse(""); + } + @Override public ConfigurationService getConfigurationService() { return service; From a8d27ef0d75920d73be201c6e8b1c83030da0af2 Mon Sep 17 00:00:00 2001 From: "Sean C. Sullivan" Date: Fri, 3 Sep 2021 06:14:34 -0700 Subject: [PATCH 0039/1608] chore(ci): drop actions/cache@v2 We no longer need to use actions/cache. Maven dependencies are cached via actions/setup-java https://github.blog/changelog/2021-08-30-github-actions-setup-java-now-supports-dependency-caching/ --- .github/workflows/master-snapshot-release.yml | 6 ------ .github/workflows/pr.yml | 7 ------- 2 files changed, 13 deletions(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index 8ad8ea60bc..09e33fdb0f 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -20,12 +20,6 @@ jobs: kubernetes: ['v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4'] steps: - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - name: Set up Java and Maven uses: actions/setup-java@v2 with: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f2ed4b6419..c5b72693a1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,12 +20,6 @@ jobs: kubernetes: ['v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4'] steps: - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - name: Set up Java and Maven uses: actions/setup-java@v2 with: @@ -44,4 +38,3 @@ jobs: driver: 'docker' - name: Run integration tests run: ./mvnw ${MAVEN_ARGS} -B package -P no-unit-tests --file pom.xml - From 76dd44235d095556d162a1cf6d412ff4439a7abf Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 8 Sep 2021 20:37:12 +0200 Subject: [PATCH 0040/1608] fix: re-add AbstractEvent for backwards compatibility --- .../processing/event/AbstractEvent.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java new file mode 100644 index 0000000000..d6d7273b7f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.processing.event; + +import io.fabric8.kubernetes.client.CustomResource; +import java.util.function.Predicate; + +/** + * @deprecated use {@link DefaultEvent} instead + */ +@Deprecated +public class AbstractEvent extends DefaultEvent { + + public AbstractEvent(String relatedCustomResourceUid, EventSource eventSource) { + super(relatedCustomResourceUid, eventSource); + } + + public AbstractEvent( + Predicate customResourcesSelector, EventSource eventSource) { + super(customResourcesSelector, eventSource); + } +} From 7959dff037381a0dd81bf52ae5ead1040f10db8f Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 8 Sep 2021 21:10:58 +0200 Subject: [PATCH 0041/1608] refactor: introduce DefaultControllerConfiguration AbstractControllerConfiguration is now deprecated to reflect the fact that it's not abstract anymore with recent changes but is kept for now to avoid API breakage. --- .../AbstractControllerConfiguration.java | 140 +++--------------- .../ControllerConfigurationOverrider.java | 2 +- .../DefaultControllerConfiguration.java | 134 +++++++++++++++++ .../CustomResourceEventSourceTest.java | 4 +- 4 files changed, 156 insertions(+), 124 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java index cc8a96fcf1..2051a4945f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java @@ -1,134 +1,32 @@ package io.javaoperatorsdk.operator.api.config; -import java.util.Collections; -import java.util.Set; - import io.fabric8.kubernetes.client.CustomResource; +import java.util.Set; +/** + * @deprecated use {@link DefaultControllerConfiguration} instead + * @param + */ +@Deprecated public class AbstractControllerConfiguration> - implements ControllerConfiguration { - - private final String associatedControllerClassName; - private final String name; - private final String crdName; - private final String finalizer; - private final boolean generationAware; - private final Set namespaces; - private final boolean watchAllNamespaces; - private final RetryConfiguration retryConfiguration; - private final String labelSelector; - private Class customResourceClass; - private ConfigurationService service; - - public AbstractControllerConfiguration( - String associatedControllerClassName, - String name, - String crdName, - String finalizer, - boolean generationAware, - Set namespaces, - RetryConfiguration retryConfiguration, - String labelSelector, - Class customResourceClass, - ConfigurationService service) { - this.associatedControllerClassName = associatedControllerClassName; - this.name = name; - this.crdName = crdName; - this.finalizer = finalizer; - this.generationAware = generationAware; - this.namespaces = - namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); - this.watchAllNamespaces = this.namespaces.isEmpty(); - this.retryConfiguration = - retryConfiguration == null - ? ControllerConfiguration.super.getRetryConfiguration() - : retryConfiguration; - this.labelSelector = labelSelector; - this.customResourceClass = - customResourceClass == null ? ControllerConfiguration.super.getCustomResourceClass() - : customResourceClass; - setConfigurationService(service); - } + extends DefaultControllerConfiguration { - /** - * @deprecated use - * {@link #AbstractControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration, String, Class, ConfigurationService)} - * instead - */ @Deprecated - public AbstractControllerConfiguration( - String associatedControllerClassName, - String name, - String crdName, - String finalizer, - boolean generationAware, + public AbstractControllerConfiguration(String associatedControllerClassName, String name, + String crdName, String finalizer, boolean generationAware, Set namespaces, RetryConfiguration retryConfiguration) { - this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, - retryConfiguration, null, null, null); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getCRDName() { - return crdName; - } - - @Override - public String getFinalizer() { - return finalizer; - } - - @Override - public boolean isGenerationAware() { - return generationAware; + super(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, + retryConfiguration); } - @Override - public String getAssociatedControllerClassName() { - return associatedControllerClassName; - } - - @Override - public Set getNamespaces() { - return namespaces; - } - - @Override - public boolean watchAllNamespaces() { - return watchAllNamespaces; - } - - @Override - public RetryConfiguration getRetryConfiguration() { - return retryConfiguration; - } - - @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public void setConfigurationService(ConfigurationService service) { - if (this.service != null) { - throw new RuntimeException("A ConfigurationService is already associated with '" + name - + "' ControllerConfiguration. Cannot change it once set!"); - } - this.service = service; - } - - @Override - public String getLabelSelector() { - return labelSelector; - } - - @Override - public Class getCustomResourceClass() { - return customResourceClass; + public AbstractControllerConfiguration(String associatedControllerClassName, String name, + String crdName, String finalizer, boolean generationAware, + Set namespaces, + RetryConfiguration retryConfiguration, String labelSelector, + Class customResourceClass, + ConfigurationService service) { + super(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, + retryConfiguration, labelSelector, customResourceClass, service); } } 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 51ff31af00..a199c81157 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 @@ -66,7 +66,7 @@ public ControllerConfigurationOverrider withLabelSelector(String labelSelecto } public ControllerConfiguration build() { - return new AbstractControllerConfiguration<>( + return new DefaultControllerConfiguration<>( original.getAssociatedControllerClassName(), original.getName(), original.getCRDName(), 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 new file mode 100644 index 0000000000..1b4a0a26f2 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultControllerConfiguration.java @@ -0,0 +1,134 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.Collections; +import java.util.Set; + +import io.fabric8.kubernetes.client.CustomResource; + +public class DefaultControllerConfiguration> + implements ControllerConfiguration { + + private final String associatedControllerClassName; + private final String name; + private final String crdName; + private final String finalizer; + private final boolean generationAware; + private final Set namespaces; + private final boolean watchAllNamespaces; + private final RetryConfiguration retryConfiguration; + private final String labelSelector; + private Class customResourceClass; + private ConfigurationService service; + + public DefaultControllerConfiguration( + String associatedControllerClassName, + String name, + String crdName, + String finalizer, + boolean generationAware, + Set namespaces, + RetryConfiguration retryConfiguration, + String labelSelector, + Class customResourceClass, + ConfigurationService service) { + this.associatedControllerClassName = associatedControllerClassName; + this.name = name; + this.crdName = crdName; + this.finalizer = finalizer; + this.generationAware = generationAware; + this.namespaces = + namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); + this.watchAllNamespaces = this.namespaces.isEmpty(); + this.retryConfiguration = + retryConfiguration == null + ? ControllerConfiguration.super.getRetryConfiguration() + : retryConfiguration; + this.labelSelector = labelSelector; + this.customResourceClass = + customResourceClass == null ? ControllerConfiguration.super.getCustomResourceClass() + : customResourceClass; + setConfigurationService(service); + } + + /** + * @deprecated use + * {@link #DefaultControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration, String, Class, ConfigurationService)} + * instead + */ + @Deprecated + public DefaultControllerConfiguration( + String associatedControllerClassName, + String name, + String crdName, + String finalizer, + boolean generationAware, + Set namespaces, + RetryConfiguration retryConfiguration) { + this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, + retryConfiguration, null, null, null); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getCRDName() { + return crdName; + } + + @Override + public String getFinalizer() { + return finalizer; + } + + @Override + public boolean isGenerationAware() { + return generationAware; + } + + @Override + public String getAssociatedControllerClassName() { + return associatedControllerClassName; + } + + @Override + public Set getNamespaces() { + return namespaces; + } + + @Override + public boolean watchAllNamespaces() { + return watchAllNamespaces; + } + + @Override + public RetryConfiguration getRetryConfiguration() { + return retryConfiguration; + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public void setConfigurationService(ConfigurationService service) { + if (this.service != null) { + throw new RuntimeException("A ConfigurationService is already associated with '" + name + + "' ControllerConfiguration. Cannot change it once set!"); + } + this.service = service; + } + + @Override + public String getLabelSelector() { + return labelSelector; + } + + @Override + public Class getCustomResourceClass() { + return customResourceClass; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 1448982379..18a214e196 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -17,7 +17,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.AbstractControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -113,7 +113,7 @@ public MixedOperation { + DefaultControllerConfiguration { public TestConfiguration(boolean generationAware) { super(null, null, null, FINALIZER, generationAware, null, null, null, From cb0870e93f7e7c8c6da39a7a1591b4fa9478ed5b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 9 Sep 2021 09:26:15 +0200 Subject: [PATCH 0042/1608] chore(ci): also test Kubernetes 1.22 --- .github/workflows/master-snapshot-release.yml | 2 +- .github/workflows/pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index 09e33fdb0f..8c1d1d0049 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -17,7 +17,7 @@ jobs: matrix: java: [11, 16] distribution: [temurin, adopt-openj9] - kubernetes: ['v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4'] + kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: - uses: actions/checkout@v2 - name: Set up Java and Maven diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c5b72693a1..a5c4c0609d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,7 +17,7 @@ jobs: matrix: java: [11, 16] distribution: [ temurin, adopt-openj9 ] - kubernetes: ['v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4'] + kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: - uses: actions/checkout@v2 - name: Set up Java and Maven From c62374255ac760b351dcf9494e86b5074258ed2b Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Tue, 14 Sep 2021 09:59:56 +0200 Subject: [PATCH 0043/1608] fix: disable colors in test logs #519 --- operator-framework-core/src/test/resources/log4j2.xml | 3 +-- operator-framework/src/test/resources/log4j2.xml | 3 +-- samples/common/src/main/resources/log4j2.xml | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/operator-framework-core/src/test/resources/log4j2.xml b/operator-framework-core/src/test/resources/log4j2.xml index 4f663da728..f23cf772dd 100644 --- a/operator-framework-core/src/test/resources/log4j2.xml +++ b/operator-framework-core/src/test/resources/log4j2.xml @@ -2,8 +2,7 @@ - + diff --git a/operator-framework/src/test/resources/log4j2.xml b/operator-framework/src/test/resources/log4j2.xml index 4f663da728..f23cf772dd 100644 --- a/operator-framework/src/test/resources/log4j2.xml +++ b/operator-framework/src/test/resources/log4j2.xml @@ -2,8 +2,7 @@ - + diff --git a/samples/common/src/main/resources/log4j2.xml b/samples/common/src/main/resources/log4j2.xml index 35a6a3ccdf..d6869ee67c 100644 --- a/samples/common/src/main/resources/log4j2.xml +++ b/samples/common/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ - + From 0abff782ca2057ae3a56454ff6e4cd7e25d770be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Sep 2021 03:14:35 +0000 Subject: [PATCH 0044/1608] chore(deps): bump junit-bom from 5.7.2 to 5.8.0 Bumps [junit-bom](https://github.com/junit-team/junit5) from 5.7.2 to 5.8.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.2...r5.8.0) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5f7ce60df0..9f21b7621c 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ ${java.version} ${java.version} - 5.7.2 + 5.8.0 5.7.2 1.7.32 2.14.1 From 2538aa4855b376f18f0994021521ae70438af9a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Sep 2021 03:16:04 +0000 Subject: [PATCH 0045/1608] chore(deps): bump micrometer-core from 1.7.3 to 1.7.4 Bumps [micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.7.3 to 1.7.4. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.7.3...v1.7.4) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9f21b7621c..18914e47c7 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ 3.20.2 4.1.0 2.5.4 - 1.7.3 + 1.7.4 2.11 3.8.1 From 1a6ad399796de00e534a7364b74f568f1207799e Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Tue, 14 Sep 2021 10:38:08 +0200 Subject: [PATCH 0046/1608] fix: stop timers when closing TimerEventSources --- .../event/internal/TimerEventSource.java | 34 ++++++++++++++++--- .../event/internal/TimerEventSourceTest.java | 22 ++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index bffd5a3447..be2da2ab26 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -3,23 +3,28 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; +import java.io.IOException; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TimerEventSource extends AbstractEventSource { - private Logger log = LoggerFactory.getLogger(TimerEventSource.class); - private final Timer timer = new Timer(); - + private final AtomicBoolean running = new AtomicBoolean(); private final Map onceTasks = new ConcurrentHashMap<>(); private final Map timerTasks = new ConcurrentHashMap<>(); + private Logger log = LoggerFactory.getLogger(TimerEventSource.class); public void schedule(CustomResource customResource, long delay, long period) { + if (!running.get()) { + throw new IllegalStateException("The TimerEventSource is not running"); + } + String resourceUid = KubernetesResourceUtils.getUID(customResource); if (timerTasks.containsKey(resourceUid)) { return; @@ -30,6 +35,10 @@ public void schedule(CustomResource customResource, long delay, long period) { } public void scheduleOnce(CustomResource customResource, long delay) { + if (!running.get()) { + throw new IllegalStateException("The TimerEventSource is not running"); + } + String resourceUid = KubernetesResourceUtils.getUID(customResource); if (onceTasks.containsKey(resourceUid)) { cancelOnceSchedule(resourceUid); @@ -59,6 +68,19 @@ public void cancelOnceSchedule(String customResourceUid) { } } + @Override + public void start() { + running.set(true); + } + + @Override + public void close() throws IOException { + running.set(false); + onceTasks.keySet().forEach(this::cancelOnceSchedule); + timerTasks.keySet().forEach(this::cancelSchedule); + timer.cancel(); + } + public class EventProducerTimeTask extends TimerTask { protected final String customResourceUid; @@ -69,8 +91,10 @@ public EventProducerTimeTask(String customResourceUid) { @Override public void run() { - log.debug("Producing event for custom resource id: {}", customResourceUid); - eventHandler.handleEvent(new TimerEvent(customResourceUid, TimerEventSource.this)); + if (running.get()) { + log.debug("Producing event for custom resource id: {}", customResourceUid); + eventHandler.handleEvent(new TimerEvent(customResourceUid, TimerEventSource.this)); + } } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index 769728826c..c3d3c7a530 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -2,6 +2,7 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -13,6 +14,7 @@ import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import java.io.IOException; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -33,6 +35,7 @@ class TimerEventSourceTest { public void setup() { timerEventSource = new TimerEventSource(); timerEventSource.setEventHandler(eventHandlerMock); + timerEventSource.start(); } @Test @@ -106,4 +109,23 @@ public void deRegistersOnceEventSources() throws InterruptedException { verify(eventHandlerMock, never()).handleEvent(any()); } + + @Test + public void eventNotRegisteredIfStopped() throws IOException { + CustomResource customResource = TestUtils.testCustomResource(); + + timerEventSource.close(); + assertThatExceptionOfType(IllegalStateException.class).isThrownBy( + () -> timerEventSource.scheduleOnce(customResource, PERIOD)); + } + + @Test + public void eventNotFiredIfStopped() throws InterruptedException, IOException { + timerEventSource.scheduleOnce(TestUtils.testCustomResource(), PERIOD); + timerEventSource.close(); + + Thread.sleep(PERIOD + TESTING_TIME_SLACK); + + verify(eventHandlerMock, never()).handleEvent(any()); + } } From 646f396e6747818aec033267d6dde91c4e1d9566 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Tue, 14 Sep 2021 13:46:54 +0200 Subject: [PATCH 0047/1608] fix: improve unit testing of TimerEventSource #293 --- .../event/internal/TimerEventSourceTest.java | 103 +++++++++++------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index c3d3c7a530..dc2f1960ea 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -3,25 +3,22 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; +import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import java.io.IOException; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionFactory; +import org.awaitility.core.ThrowingRunnable; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -@Disabled("Currently very flaky, will fix in https://github.com/java-operator-sdk/java-operator-sdk/issues/293") class TimerEventSourceTest { public static final int INITIAL_DELAY = 50; @@ -29,10 +26,12 @@ class TimerEventSourceTest { public static final int TESTING_TIME_SLACK = 40; private TimerEventSource timerEventSource; - private EventHandler eventHandlerMock = mock(EventHandler.class); + private CapturingEventHandler eventHandlerMock; @BeforeEach public void setup() { + eventHandlerMock = new CapturingEventHandler(); + timerEventSource = new TimerEventSource(); timerEventSource.setEventHandler(eventHandlerMock); timerEventSource.start(); @@ -41,73 +40,69 @@ public void setup() { @Test public void producesEventsPeriodically() { CustomResource customResource = TestUtils.testCustomResource(); - timerEventSource.schedule(customResource, INITIAL_DELAY, PERIOD); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(TimerEvent.class); - verify(eventHandlerMock, timeout(INITIAL_DELAY + PERIOD + TESTING_TIME_SLACK).times(2)) - .handleEvent(argumentCaptor.capture()); - List events = argumentCaptor.getAllValues(); - assertThat(events) - .allMatch(e -> e.getRelatedCustomResourceUid().equals(getUID(customResource))); - assertThat(events).allMatch(e -> e.getEventSource().equals(timerEventSource)); + untilAsserted(() -> { + assertThat(eventHandlerMock.events) + .hasSizeGreaterThan(2); + assertThat(eventHandlerMock.events) + .allMatch(e -> e.getRelatedCustomResourceUid().equals(getUID(customResource))); + assertThat(eventHandlerMock.events) + .allMatch(e -> e.getEventSource().equals(timerEventSource)); + }); } @Test - public void deRegistersPeriodicalEventSources() throws InterruptedException { + public void deRegistersPeriodicalEventSources() { CustomResource customResource = TestUtils.testCustomResource(); timerEventSource.schedule(customResource, INITIAL_DELAY, PERIOD); - Thread.sleep(INITIAL_DELAY + PERIOD + TESTING_TIME_SLACK); + untilAsserted(() -> assertThat(eventHandlerMock.events).hasSizeGreaterThan(1)); + timerEventSource.eventSourceDeRegisteredForResource(getUID(customResource)); - Thread.sleep(PERIOD + TESTING_TIME_SLACK); - verify(eventHandlerMock, times(2)).handleEvent(any()); + int size = eventHandlerMock.events.size(); + untilAsserted(() -> assertThat(eventHandlerMock.events).hasSize(size)); } @Test - public void schedulesOnce() throws InterruptedException { + public void schedulesOnce() { CustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); - Thread.sleep(2 * PERIOD + TESTING_TIME_SLACK); - verify(eventHandlerMock, times(1)).handleEvent(any()); + untilAsserted(() -> assertThat(eventHandlerMock.events).hasSize(1)); + untilAsserted(PERIOD * 2, 0, () -> assertThat(eventHandlerMock.events).hasSize(1)); } @Test - public void canCancelOnce() throws InterruptedException { + public void canCancelOnce() { CustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource.cancelOnceSchedule(KubernetesResourceUtils.getUID(customResource)); - Thread.sleep(PERIOD + TESTING_TIME_SLACK); - verify(eventHandlerMock, never()).handleEvent(any()); + untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } @Test - public void canRescheduleOnceEvent() throws InterruptedException { + public void canRescheduleOnceEvent() { CustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource.scheduleOnce(customResource, 2 * PERIOD); - Thread.sleep(PERIOD + TESTING_TIME_SLACK); - verify(eventHandlerMock, never()).handleEvent(any()); - Thread.sleep(PERIOD + TESTING_TIME_SLACK); - verify(eventHandlerMock, times(1)).handleEvent(any()); + untilAsserted(PERIOD * 2, PERIOD, () -> assertThat(eventHandlerMock.events).hasSize(1)); } @Test - public void deRegistersOnceEventSources() throws InterruptedException { + public void deRegistersOnceEventSources() { CustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource.eventSourceDeRegisteredForResource(getUID(customResource)); - Thread.sleep(PERIOD + TESTING_TIME_SLACK); - verify(eventHandlerMock, never()).handleEvent(any()); + untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } @Test @@ -120,12 +115,42 @@ public void eventNotRegisteredIfStopped() throws IOException { } @Test - public void eventNotFiredIfStopped() throws InterruptedException, IOException { + public void eventNotFiredIfStopped() throws IOException { timerEventSource.scheduleOnce(TestUtils.testCustomResource(), PERIOD); timerEventSource.close(); - Thread.sleep(PERIOD + TESTING_TIME_SLACK); + untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); + } + + private void untilAsserted(ThrowingRunnable assertion) { + untilAsserted(INITIAL_DELAY, PERIOD, assertion); + } + + private void untilAsserted(long initialDelay, long interval, ThrowingRunnable assertion) { + long delay = INITIAL_DELAY; + long period = PERIOD; + + ConditionFactory cf = Awaitility.await(); + + if (initialDelay > 0) { + delay = initialDelay; + cf = cf.pollDelay(initialDelay, TimeUnit.MILLISECONDS); + } + if (interval > 0) { + period = interval; + cf = cf.pollInterval(interval, TimeUnit.MILLISECONDS); + } + + cf = cf.atMost(delay + (period * 3), TimeUnit.MILLISECONDS); + cf.untilAsserted(assertion); + } + + private static class CapturingEventHandler implements EventHandler { + private final List events = new CopyOnWriteArrayList<>(); - verify(eventHandlerMock, never()).handleEvent(any()); + @Override + public void handleEvent(Event event) { + events.add(event); + } } } From ff28baaf8427a2253aeff667a17f2264ebec9975 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Thu, 16 Sep 2021 14:18:54 +0200 Subject: [PATCH 0048/1608] chore: generify TimerEventSource to reduce the noise about raw use of parameters --- .../event/DefaultEventSourceManager.java | 4 ++-- .../event/internal/TimerEventSource.java | 8 ++++---- .../event/internal/TimerEventSourceTest.java | 20 +++++++++---------- ...entSourceTestCustomResourceController.java | 3 ++- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 47542eb96d..f71a4feb47 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -34,13 +34,13 @@ public class DefaultEventSourceManager> private final ReentrantLock lock = new ReentrantLock(); private final Map eventSources = new ConcurrentHashMap<>(); private final DefaultEventHandler defaultEventHandler; - private TimerEventSource retryTimerEventSource; + private TimerEventSource retryTimerEventSource; DefaultEventSourceManager(DefaultEventHandler defaultEventHandler, boolean supportRetry) { this.defaultEventHandler = defaultEventHandler; defaultEventHandler.setEventSourceManager(this); if (supportRetry) { - this.retryTimerEventSource = new TimerEventSource(); + this.retryTimerEventSource = new TimerEventSource<>(); registerEventSource(RETRY_TIMER_EVENT_SOURCE_NAME, retryTimerEventSource); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index be2da2ab26..cdfc979eed 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -12,15 +12,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class TimerEventSource extends AbstractEventSource { +public class TimerEventSource> extends AbstractEventSource { + private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class); private final Timer timer = new Timer(); private final AtomicBoolean running = new AtomicBoolean(); private final Map onceTasks = new ConcurrentHashMap<>(); private final Map timerTasks = new ConcurrentHashMap<>(); - private Logger log = LoggerFactory.getLogger(TimerEventSource.class); - public void schedule(CustomResource customResource, long delay, long period) { + public void schedule(R customResource, long delay, long period) { if (!running.get()) { throw new IllegalStateException("The TimerEventSource is not running"); } @@ -34,7 +34,7 @@ public void schedule(CustomResource customResource, long delay, long period) { timer.schedule(task, delay, period); } - public void scheduleOnce(CustomResource customResource, long delay) { + public void scheduleOnce(R customResource, long delay) { if (!running.get()) { throw new IllegalStateException("The TimerEventSource is not running"); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index dc2f1960ea..62585f3255 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -4,11 +4,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import java.io.IOException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -25,21 +25,21 @@ class TimerEventSourceTest { public static final int PERIOD = 50; public static final int TESTING_TIME_SLACK = 40; - private TimerEventSource timerEventSource; + private TimerEventSource timerEventSource; private CapturingEventHandler eventHandlerMock; @BeforeEach public void setup() { eventHandlerMock = new CapturingEventHandler(); - timerEventSource = new TimerEventSource(); + timerEventSource = new TimerEventSource<>(); timerEventSource.setEventHandler(eventHandlerMock); timerEventSource.start(); } @Test public void producesEventsPeriodically() { - CustomResource customResource = TestUtils.testCustomResource(); + TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.schedule(customResource, INITIAL_DELAY, PERIOD); untilAsserted(() -> { @@ -54,7 +54,7 @@ public void producesEventsPeriodically() { @Test public void deRegistersPeriodicalEventSources() { - CustomResource customResource = TestUtils.testCustomResource(); + TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.schedule(customResource, INITIAL_DELAY, PERIOD); untilAsserted(() -> assertThat(eventHandlerMock.events).hasSizeGreaterThan(1)); @@ -67,7 +67,7 @@ public void deRegistersPeriodicalEventSources() { @Test public void schedulesOnce() { - CustomResource customResource = TestUtils.testCustomResource(); + TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); @@ -77,7 +77,7 @@ public void schedulesOnce() { @Test public void canCancelOnce() { - CustomResource customResource = TestUtils.testCustomResource(); + TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource.cancelOnceSchedule(KubernetesResourceUtils.getUID(customResource)); @@ -87,7 +87,7 @@ public void canCancelOnce() { @Test public void canRescheduleOnceEvent() { - CustomResource customResource = TestUtils.testCustomResource(); + TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource.scheduleOnce(customResource, 2 * PERIOD); @@ -97,7 +97,7 @@ public void canRescheduleOnceEvent() { @Test public void deRegistersOnceEventSources() { - CustomResource customResource = TestUtils.testCustomResource(); + TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource.eventSourceDeRegisteredForResource(getUID(customResource)); @@ -107,7 +107,7 @@ public void deRegistersOnceEventSources() { @Test public void eventNotRegisteredIfStopped() throws IOException { - CustomResource customResource = TestUtils.testCustomResource(); + TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.close(); assertThatExceptionOfType(IllegalStateException.class).isThrownBy( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java index 7f42429b5f..91e4e33298 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java @@ -25,7 +25,8 @@ public class EventSourceTestCustomResourceController public static final int TIMER_DELAY = 300; public static final int TIMER_PERIOD = 500; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); - private final TimerEventSource timerEventSource = new TimerEventSource(); + private final TimerEventSource timerEventSource = + new TimerEventSource<>(); @Override public void init(EventSourceManager eventSourceManager) { From d80596c3041b8b9da76bd6271e55012cf44947c9 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 16 Sep 2021 15:18:41 +0200 Subject: [PATCH 0049/1608] feat: make it easier to override ConfigurationService Fixes #542 --- .../config/ConfigurationServiceOverrider.java | 116 ++++++++++++++++++ .../sample/PureJavaApplicationRunner.java | 6 +- 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java new file mode 100644 index 0000000000..64a2d5c050 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -0,0 +1,116 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.javaoperatorsdk.operator.api.ResourceController; +import java.util.Set; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.Metrics; + +public class ConfigurationServiceOverrider { + private final ConfigurationService original; + private Metrics metrics; + private Config clientConfig; + private boolean checkCR; + private int threadNumber; + private ObjectMapper mapper; + private int timeoutSeconds; + + public ConfigurationServiceOverrider( + ConfigurationService original) { + this.original = original; + this.clientConfig = original.getClientConfiguration(); + this.checkCR = original.checkCRDAndValidateLocalModel(); + this.threadNumber = original.concurrentReconciliationThreads(); + this.mapper = original.getObjectMapper(); + this.timeoutSeconds = original.getTerminationTimeoutSeconds(); + this.metrics = original.getMetrics(); + } + + + public ConfigurationServiceOverrider withClientConfiguration(Config configuration) { + this.clientConfig = configuration; + return this; + } + + public ConfigurationServiceOverrider checkingCRDAndValidateLocalModel(boolean check) { + this.checkCR = check; + return this; + } + + public ConfigurationServiceOverrider withConcurrentReconciliationThreads(int threadNumber) { + this.threadNumber = threadNumber; + return this; + } + + public ConfigurationServiceOverrider withObjectMapper(ObjectMapper mapper) { + this.mapper = mapper; + return this; + } + + public ConfigurationServiceOverrider withTerminationTimeoutSeconds(int timeoutSeconds) { + this.timeoutSeconds = timeoutSeconds; + return this; + } + + public ConfigurationServiceOverrider withMetrics(Metrics metrics) { + this.metrics = metrics; + return this; + } + + public ConfigurationService build() { + return new ConfigurationService() { + @Override + public ControllerConfiguration getConfigurationFor( + ResourceController controller) { + return original.getConfigurationFor(controller); + } + + @Override + public Set getKnownControllerNames() { + return original.getKnownControllerNames(); + } + + @Override + public Version getVersion() { + return original.getVersion(); + } + + @Override + public Config getClientConfiguration() { + return clientConfig; + } + + @Override + public boolean checkCRDAndValidateLocalModel() { + return checkCR; + } + + @Override + public int concurrentReconciliationThreads() { + return threadNumber; + } + + @Override + public ObjectMapper getObjectMapper() { + return mapper; + } + + @Override + public int getTerminationTimeoutSeconds() { + return timeoutSeconds; + } + + @Override + public Metrics getMetrics() { + return metrics; + } + }; + } + + public static ConfigurationServiceOverrider override(ConfigurationService original) { + return new ConfigurationServiceOverrider(original); + } +} diff --git a/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java b/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java index fae8f88c9e..9cd639e18b 100644 --- a/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java +++ b/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java @@ -3,13 +3,17 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class PureJavaApplicationRunner { public static void main(String[] args) { KubernetesClient client = new DefaultKubernetesClient(); - Operator operator = new Operator(client, DefaultConfigurationService.instance()); + Operator operator = new Operator(client, + ConfigurationServiceOverrider.override(DefaultConfigurationService.instance()) + .withConcurrentReconciliationThreads(2) + .build()); operator.register(new CustomServiceController(client)); operator.start(); } From d4d34068802446165f67bbba5b8a1ff46f91f786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 20 Sep 2021 09:08:20 +0200 Subject: [PATCH 0050/1608] Put back the missing logo (#550) --- docs/assets/images/logo.png | Bin 0 -> 35366 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/images/logo.png diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..220129ae892c7d6b7de2303a07438a4e96494bc0 GIT binary patch literal 35366 zcmeFZg;!MH_dg645`v0^f*=;spmeI3AkEMWCDI`cLrN%8zNH(bhwd1=)DkUFY9e@s%?&%6r!a>neD!Xpxrk_gHJqqD&8w{7mQ> zsgiitUgNXXn!ehkD7rM)*CMnB{kN%m_U=O;cL*Q-r+yU_uRZP@9)6!Pp)&IzIJ6q; zehQfR|9}232arF_(dcok<^*wDh>zhJV?1@_Zg28tZ|8G34sprHX7>>V4WCUVYn>xu zuG^aR_je?|&6Yi`apeo?%h+tv?EEQ|G)!~%Pa6p%KO6hlV+tdf^$~9nQ%#H7#OXgx zhG_vwadpoj5>o;}u|e_S;WPX7wX`Sw{BJL|Ew;x=b0+!}m;9WHEG7Tb()6$$bN%k> z;LNT@C|t=DC&m%TuIY-K<$#@ZrHYbMe5F^WFMM>P!@kk_WV87VA1l7n37wBFSW~5t ziBdTE{M@Uq;O>3&xDRa5YbpVKIJo>K@AC;AP3O;x321XuXJV1@1RRjVxd0=oXEp-BrEQNd;}$GX06 z9+t{1$vyRdx?r&~l|_M!h0%yucjbl?X7Qf_h*z8vW{ri%eiS%@{ae+q2D$lX6P~$M zv(hTvCwww|0CKfsDq1ef2dWBEA-C|p8b853sx31Tt!1dDq zeSLGW?e+m~y>ZERc+3;MWK8RG?=OQwKL7-Ln%q{I@XVC)+UJW-I_dW8MOW}30pP1? z!P?2YEP3Fa#8JfrF#Fh46R&g=l5dQi8pWvM* zHa>q24`XayDkZ1_a_6(ZZNMo;`g=^0(6~>u>2$w1s!BEgXyVMD_l6n3WSP$)Q@U(z z>!!Hq|05=$Q9!3Fve>s1p8gtl*#C6#{g2kcILWwLr4q~&_+HbMhPs#KPZPj}< zV6|b4QxQGO>zP}>*?uC$hKoR3paII-K#^nm9?b6RKh6gLT0!xbx&N2^SAeLdUzFXe zwrySGX43073of=+loE7Q{>f`MAg^1-=KbpJ9Pf(G(L0uy30SBT0pcf8HuRUP{b}SfQ9LIbboW_T@S~3< zhdI_MFD-TdDa&2X7&Jd4QD!}zMh>o+RO?B5M7DCJZZ>LGo(vqv7@fqJl%BraA$CfU z?SBP60FY@>fq;nK<@mO{N6Nx~djJ%kTOu0S5A!+`QjwKX?ppu1@lMdP9w zU2MJM$5ow)6J0U<8jz$_!#G6=`l~*MQs>1=REeCN_L@pTCmYDA{8PwaKp`{3!xNZH zv5x@(kyj>S9^&I0d>LplJ`Y%CN(pffd7-~LOb&E0^*-v{t+*&lLz=nkG4Z$i+(Ta|CRb)}*-|#!5`Z_EMj8q6JK|9AKI!!UeYJUx|xb z+WzxlMB#}3z;;k!z55k?{MS^pUg~w#N+9x4Kv*UkF{4*I(tz#X^k+9^_%o_+uyS`W zoq1*Nv!ZaH0_^Nx=U)L(Sj*V@+_cPq!&9&FgowC{Z0s>>gEO;5uCtZPkoqZ)@~ZTN zzxESQq7rq1N@u7joj?}!*z2TQ3||ad8NugT$;Zkn(inxXRJD|@t|bhR{UhW*weH|d zG2*J=P-AoS|E%p{S8!%9o2Q=e`p5an^!nt@(i7{{)McUBsd{h_v1w&9b)e-EJHIuQ zxqJA_(GgDO4h7%<_tPoi-rOJCumV5{H2%4PHei(>qgujBYXk*?1{}}kmYzsv(+hGj z6$EiMqP2&q+c5nPNkpf!X8ejZ8cz3NoYkj3-R7@%{{Wy9s9SkeJ)H?wZ9+ddswFZk zY}emaM$Ao1`mgXkQFr)S`UYm5|H?LcJVg-qKX&-m;uDZS5M3n{@{DSn1-&`V0r|TU zz@Cf}Ek9y~Xm!us2+~~|II%=cAI^)ZD9EiAnL6@M3L^-3)~WIT8TMkY>&ocA4KR8+ zKc5K)=ptf*)RPJL4LQYB+J6?oT_%`Q7z?kn3gLSG3e#!!#V6N^xaEQxl?aPZ#jTZ- zsfplZ8%1=sy`!IdP@imMCa132wsV{e`z9c1MgJFeX$}#Xrgb`D1EVk-yGJbGX@xs~ z^ylwiIr$#Ieou-VKp8~NOHkkkAPz&GeE(*91;+zrw{GXA1=>Q>P5t=eJDOdkT1q(n0tqG%PUM&N;_=y3e3i(7^zxNjv zhNY(c6>|IYVuK-3Lk7Leuh0Mc-XHk>1;?VCV&p%@6ni{YZJbQ-?KB%ZeT?eC z7we&50Z;H>Q@;YN)Jv3mHhxMKeu8V$WPt5Y^HLVRr|x(%=t{&VT1r?gL>`xFoTCGt=ooX4;|BVC74z zdxjP)KJ||4D>-&Y*oK8WL7E_5OVcqPIuum=5 z=d(h(WvUhy_ViDpzjJ^?nbAieWX^L;^T0^dqfRRlgFv<*KrDE-0Vr?do6`r)FNymlCj00_wMgc_-{N`RCin|J4|G=)F)H!&96`JRclml z`@Mg@kAT%fxl&QH3c2_jnOhm2XCBh9Y;Jn~H zoYg7uJyvP#xpe@{_ecteSv~@?w0tU9M$FPV(3V$X7V)v5f^ zSq80eMkdnikHR88j+?*Z0$)D>FA=-Et5V4WsXOHxLf5r$1yvC0nGlB@r zZkSqJ`yQF4Ip9`9QUKi{3ZKhwi{lG$(byDFmdsDBijh|m_hcu|tSC{9_s}20yqs}! z#V6u?`ZD0t5L4I74nm*uU<7oOzAspEl0evDoI>~VO7(o1LeA!CFvtg*EHq5nsS^^YXGC%`C9ujDyH#0LWat%itt8kBUYdPMOlAF}Tk~ ztL_(W3`Mj?B_wLP9^=zCerpv!n`zjlVpWaMaX_rxyf?-CtSm3pjm0*@R~#!*s)Ur7 z=MQIV*B2QO&KPVd%=h-U$RDlK8rw>#N9*2yq|rCn{a{qF|AzGdPBguhabc6KFdaan z_lD1lXOd}3!*Gu#NLLj2*w~|&e{)jnbOg=?6~Jg4t>4chRqe##C7L1ZqJXFN|I`a` z(>4a7BFEvaD@=OMY)11Bz{y%C>LjHo4<}eg+|qmhmscd?Vqa{V3l&=#Ijl*wwyo4* z{bTi;KDK@H))Ur#7&?#Xeu+I)ey0fFQ&nnl=UL2njuZRdAzQTNAG6}?j3L?sKqE|J z&tBkI0SvjJKHgeJ)A1VUTdu{VUC$rkCnx^A^^@w0X=;$6>BO^L8bp+wNR~Y0 zUn=4B9_{V0^>=?sUdd98hesp~^v=iuA&FZoZzhc_MJo+W`@cAqUqN6R0ABT=-5ABL z;ARMNw>k34B#jz{QqOaLr`1gt^@5$>JR>o*6c+Yoa;h}kFxJx-Vg~}md155&TJ5dn z@}51AAQlSR{qw0Wof|IF|Gg~V*ZU4rL>R1B84CcN!Cyd)EojQrRoR%f@voh1vh-4G zP;35$7W6oQftPx+39gQ#8Kn|e-V-BZS;B)8zUoX)Tce@N>MBe}s+BRsL2Jq=OI9D_8{rY}9D-I~iUN zjQ$fv_vJP-+f+^IQ#o>7e_WcDI_p_ejY|$gh2bh(1wR_%(L9==^9{Zyfa9ST5Nv~U zUwm#=lpht`lxqR~Sq;fLV5MKa$gR;9j$PV9C{eS-*qk(R0?K@O*^6Zu;~~{CTHbZy z47HvEiKJ(878avPy!8l{e=wU9h*s_sV>y@Em`l>)ZbX?nF?A#lO@Ur=a+|THWeJQ6 zjVIQXghU?D`3&wa3B!Nf)*stQ(sUV+T`>-!*%zl$VV@PI2U zF^H53p}Z$2ZCjUtSyUD*deLpA(MW^n<;z0SONq%X`sZ{p+k7mxNo?+PZjVvPz56jC54{L9mH1JmT^ z!N}etstzjRuCO!^x|>`71sk6M>*O4w>1f$fWqJ}O_!3DNQl&W773SL*n%exItj}3s zwCXp5a*AX`p;oOtS)~>!zyTYRg{r_)asnMbu?ifCqd4Q^42{W!lGXRPj`~_i*0DQW;xwu%+Yl`gy^f2pR?vVu8FFi=Tb&>)F z%`6dV|4Z(Y0IM%&Y)J(Y3mF&xrKpGq#!f4ii!# zzax{W6W$P=ZKU%r_-6K575hJ2cu3qdBGy>eW(Fe;k^X;$w#l~*$P_OTqv(i;M1vj_ zQ|6V1x?0%r(}}Z;8Gr{hXk(P-gE9Q@AS3yfI(_#LSfgg;OWgenB*nIv>4U*R-SHlj z{DF%b(f@9c+`?pGA&%Ip_rLbjg_N;6g>@ za=ixp>G+)hE&-8bqd|lMgr{OYI;4HTs@4zdp%^YrOtj7ypt!7N_by&e+`3%>Z>^IW zCC~OeSVaFaDIV|u4{6)19Y;|q7BJ4$XO}T<%^kb(dVQkk4tAmWF=f$x8UZ1E_2fr^ zL(`+xr|dpM8*cI(NA@0y6pu(lhb_8x;3fk#W}U``hoK1c0&3SIbl#_SPyr`-8H=@W ze zkB5*U&zUeeyQqkMzF!Eq)`XDedH*H${nWCC>-D{ysrs(PpJkXcm?}Ut5GCilQjR1Y zLVhsLR2Ofn-ja5qmtv`%J&r{AKx8s2VSJ&kq|NS+X615LJ0!oAf4@BtztnwvktO5X z0VMhEO^CD)!MXN^8oeI&_0KCUC1O{YOt9`a*R}AUWkEfiw`Qkm?W#1r3{vae7+N|R zSW-t+9-qM@w;|tDD*~~rJIo1P5BfIT3&UJvTn3oKw$m(zHi01i9^cha-5N9Gf;3qJ z7VU8rx7q5Dav%|~+bkZK@`lehP$8tH^brRk0F(B>l4kF#^N1kjJnY80r@-=&zjy*Jo(vvZ9pV%7)3@nSY15N5m)4l9 zx^g*YjqYQ@=JG}SBuN>^K{aoU{;aCEB4r0Y%%3HAL}E29ltqp4T-+IX!3fOASP|=?Os=fMG)cmy}~b} z9hXl>O_qPQ+Oz&*?@5}?eOzM~a0TVm*{$R*z-Itirc6zO0<42Vf+oath4%frN#jt7 z3yY(b;XXFca4@*zdX4RdEEMltD5I7pGkuRw*F_FK2xvP?T9=q zqwsrgY>E02-MPE73t0D*fov2mwo@Sw78C0TgJmB?9lw&j=F^87Snzz~KDL3sV=$o9 z=^oKIQzNwW7K|Q9K=|ar*fko3ku?o+Ch!|`ZC3zyC}v~Xf8a|idl6tO-94INI+mQN zTt%m`L}u2Wj2$s+>jRDXc}Xy9>uqc8d+{5uM`}i-T&l7ex#umaS3@L&rFE;BcJbxW z4A(KK&Xz{m+Mo{{D}}NHXNH0rK1ualY!7U+w-KTgS)hDSr;fw+j;BmK{;ny~n7Sr^ z2C35VVKh9q1We2B$Wz3TId~9?sYw~YYwtl;aG|IpCKJNpNTEvOQSICMu~WFu@W+j1 zO~aC+Flm!deiB~vJ-9tvSLMc7hYeSstT*(PDL%D20$Fp_8nyyqB@7aDd9f8Y5g{jd`7gz`HvpE|Bai)2_$UYlyO=k<}X;}|Q# z1}tf2B}e~fO^C_|${xOo*me*Wd=H8h@Ird&8q(^>M!Ao`3!@EC$D$DY}(fE$JUp1Or!lK6Zo!7A9%|} zS9RPI-rT1Ds0lqozUzX@UnuM9(K?!5pUo@Z;2H~m)WZ|vFdAj+xEM0^)1&a3 zU_Aexpw#tS1=7`}-k2Hzn)~4^wNiE=O)s53kC$Fxu0nx zfygUk@pGXmHca*WEC6VIA0_bIhOb%7VgW@lR^6Rt02q{2ie0O-W_<# z14kwaN48#pMtY6JJkh%H3Fwcu*MRPDtTGp@35!e}%^s1FtPGaUtn|}9H%WYbFr0l{ zYmjcLZyiLS_X=w-5;%8^Eve0KXXIJ$uxfA{B!VqPkY-ISuEe?}NT> z%CJsO5)?D9p|?@!e=uHsao#fKw&rTrqiIxHqAJOOtAL(vw$%vZ)muEB8}2~Lua<;-ac$12H7^SLYhRaO#ZF zo%RZ8==rKif$L;EFjy;rLR(fOFNi9zsgjH_+n=z;z znc;=;YXi{kSz@O&-!xE*jH=e|J3JsUKOnMXGyRChxt_B#mSNs$$>eK z4o8yufCU-omPVg>pZ(1FxjFE4pH-RYm8ow<=`d-O$$!Gl?9>Jkb z0~7BQ597{WvV!Yo*vm9t)Y&y03P|$U(u=cTdA-j;7ANPk4nd3~u}_2dIt4v&?nkbe z<9_)3k7XHH?185NRO%iRu!Dj={)Bv!#G`y;HZv6=ow3Y|=XU5mFZ7sxJYd7b%4F3_hKHqb5bGkdSMsDC# zQ)0o|1ytRq2bICI*Lc=314b_j2$t%6Lr|2bI*73nRdbNyJNy8%h<$rdxFNvrt~xcX zC6Y2w+zIz0!K zvYI4vY1&L;+WP7{_ix?xOYblPg=2Lw!p`|jY}uN7vT3V%$*JK$#3Pqx0glf_leSvl zEL9QSIn^)z^I~ESNdGB5sT(QhZJ3mNtL&QrevL)7_R?yyC-suOAvyUBt>-DqcS*exA5=RnsE#E5vNVY&Rq-U(Ry!te+dIEt1>$dZ`l1 z)w#GvuLyG-+H-Qlpq~W3)9l4wh0;-tbuLLbGwWQ`4HNy(1R~v8(PH1F)iO%gF&A0( zHFYq5bDT4k<2_H+(+A$nIS0(V0WRJ^r9uysWgJL#nohhn#FS zf`8^R-9Utt;5o>s&6-<_Ty5i%$PD$VjJV_2yLy)IjaX`n1pIs+w)WJ-8YB{6GUPb9 z)y4?A=jN~1{>A$K4SY46?PIC@K&^n^*%DCj{N$BoI}PxJ)9v{+dAlkyuZZUC+sD?J zft(Fqh1@G&jOW(d>`h*5cH{L~ZiU%M8=tRDo!aI!%(T}u_gc}3{@H&Id8yV8)ZtP> zZXm9y_1ucq!9!r=&yt$T=lM)S9+;~+X3c(?soz<=Um8u%;BY5|Nz6CS{{uqoAmke@ zNu-FcNL59CqLorY^fB*e4d(~`qIWCunWFG5BWo#0WJvAmBWkt2#sr#_;{=}>soq7b zjWcbyKp#{iR_feqPCC1DW|cQ8(RZro#MdPe%5eJiWxnRZ5yqC$n)@pq&j^pIEQ~hK zx+i_|5zfm=xh4yni~(^h(tC-K#CARwF<@kMCY1dgL`a4w9Mfpr%5>`dalr39ushb- zKcD}U#KS2r^=*&X(R*YXWa5y!TC(OQ*@$5%wzga50hB=i%_WbczDkeiHC*bWL^pD z0+wucIlC058p~UO4qcr|I8DQmiSDsApf2*PJO)pIYdL+gub^BPe$e#J+{<^X!zV>e z;_BE$sA|@%a5fVlUTT=xfL+3OX<|CpQDJO=%RzA5tU2aRRm3VPOsN)`i3@U47~o5d zhD%=u4ytr32fn~NRa}o1e8}ag`+3$ARIR2*ulH7|P3gU!hkl;bn9SmB)S4g&M76YD zNtiN4_a)rbx1Dk-fGr=?NK61h`Te|^FbMd8ruMowIIrlPNNG)<_D-7In=i=?=wt@T zs{S<~rGf}cJE|bV%bp<0c(4}3ckQuw*}C+`(kdY!UMT* zT*UFJFa}>={??TR=F%7o6f4;&uUETk93?x=HmrsR`-g;Ayg_=HV|(RmCa0~6RuzUD zd}fdZ*PxPR+b+izW;nzFRitWN<;FU)3?uXggsYFuy?+<2hqpd{9RE?rCk5|5(O?(; z@#)Un(83?${JEcotuLi1WPiyrRdU%i3!tRQa~L}tfim_PPV{tEPvoxuVC&16u(kP= z{<qSAEvhMOF^YwFiTnKs-TSN zR_`@}`wFO*fa2aRu9=wGy!Ar==m0-~TrfSl9(3hQkx<9xLV_f#e3Fw-K-$4%zvko$KaNtEnd3f;?``c_(Q&XR zb>}zGnQ|TvOvSVqa*R`uEbynfXdxnDG0w@+m3q0wO~M^-f2M`E;{({ZqL8GucJg?s zK6*5!ZQcy$k(}R6Ifif4o`D+l|4^W+S{}`CORjT^WB@tPHKK3^jZdEyN>j^kWh)&= z8{~M(7!3D32ZcHFzHy1Ts{GuStz7Yqq{BmV?fEp@SJP5zi))t%2HSL=_i%H34l6kX zG@t39@Nu17@kfy^lX*#Ro`Npu`<8?^tw~4PAZk(Pg&&DtWTeOeS%L@_FU{&WFsaG1 zXpdGtrD*bi5bs^<<9fs#nff61`{i@mGSgQzI{2rtnUcpy&XO-)jmvFDVD4Jr zywj&axtc0080pwWS#alrhJx)a4Phe=j1W(zec6VFqXTB_9@1g}bAL(of)!QU77u%9 z>ghQkX*UKfjZD>a*y;8x(0LY@h5h+G1)|CE{*4GwQDJ#(>(Dz|+HJK+p=(;^BlhIy zg!iaGt3|Z{*$U=3b}jHw^unFx^_|a!>F(EEf}>+P=l62(bz{>{%LX-A2U@>{-Cc~b z8_T9ktPN?YCzn~&ug=~SU$gyYCrSMa%c~RXeHrO{QfMT->KAz4keoC7U}s5V`vrLc z6JUj=6)h7PijlF+c`KQ5ai7KyB;01A3<@08=~=Y+U7gImFj2?7xa6|a>o(DSb4G0G z>^oG0s?8}eB@c+O>BJkl%bc1{9~C5e+E+<6?$Z@6zrx7RSJ11bMx%p!q0IjkfqKsm zFQ!iYEP6MG6rlMk=zh@CQ!(1{ABUj7g)_W;aoxEgd>J`~QBlx^iXGb0KTL8>S<`nl zI8eQD@f?doPo$k#=qFinfy!A|R)u`yVM@%Cwal@m`B04P(be$xtWYzc8I{>-in>H0pc)4Rf`5w{Orwb|`25tE;f_ z3jtBS!=-sC(N!6F=aMm4?Dv*DS$?~Cl`=%DE^yk7kjzjSPV+cDQgvMsKBTPjg!;l;Wo?SDn-c)ZKiN7<-4p*I}F zDlE9Sb7QC8Vg5$54%6$d)pAQU+vl;Z=Mm3`Q@6ASpRC0tCnXs<2nX5q&Uf|8`kd$^pr;9m3I*~g@PeGydD!HZzRd%fkXky}ZNGsKqTxjvl+yV)U1cq-K zcs*&zYAyhc8C&yYFPfvB*=o*Py9>kT`)ruT!GdZ}bnIr@AQ0Vp<(EE&hSe}T4Oc(PlbVdpthd$Gr^ z6r1lsd32%tb>e40lV4gs7H}5xt9u1KgfxK9YhrbFIx|-(4FfplCxLGPvan4(QGJpqAMOcZzP04geKl*|e< zK662xs+NN9E}Pv+qrp7A*YO?kA^|`um8_1{-5D+TXmZrJ%v`p;kMOrQ95En_19_!c z!A3wbiB^_31In=?_^9Kd4dd*Yry|tcy7k)2Q-@~c*wk@G-O(NQ4fqt}?)RAIpuo&L zZ|W?2*YiAAF%PV*ACFf3=%fc<|A?B@TiW{;9ykh}`1n>*Zl`PJ-4g{`h*vMTI$<0t z$_o#f=g54d9##!xptT*}HW2KXSoK2*U2=EU8IrWU$2K6Ln6Kr$XJ3KYf|eC+X$@Qm zPSrINg@X1}vSmY`hVyw4GyM?=a-X89 z-}mAxg@;yw^nGOBGaf|(wMQE}k3fNU^4zJH&z9BMYq)3@s>|o#UI}$-j zdi*u^w1n4tnW^1HBiF$d^wWp)mf$aFv+mfvH~081#UkS^ZPqbgolt_RE@FWGZu^)< zlCP%JAL$9l$7%0iDfrH%=pe0D^EWLr1$5=$@Dlj?BO5Mqu_f~LY@5~nlqQ4Ec-_+g zD0W)xc?F=EW8*juvUv5&&~%te!>&T-0fD1HPOH>&!xh;C!6&Wsqo)wQtG3dbJu)om z+TVd9tbT@GYqir&`zr+aMqSQOSl~XDV`$;pbB@{Rby3N{;;4^hLf&KRGf-Z6o8jxU z3PmQWAi9`*)G4%xY$AW3;|0K^3+Q33PrpV~Jd;SspWQ;vTT6@X>TG&iRob89%NbnW zS5Glu)fxRTbSZt(L*5dk$sBrxXF2s{X0ZcqW2qgzD)*&6x#%a5N?Ld0fe(xeRMw=) zAD>foz83Uml`Wh3N|&D>idQUwtPJ0NgnOt~*gtY1XSUlfYq;mtz9FwB4(7IbT94`x zWfNU5@`?9pd=Khq^YT%M*xtw?$~IhJ?~2LZ0Z_(1m~ok;e*Nvdbc8PUYfs0sDfD;s z-rA-5FpYV$dQ`z$sU%x@4PLhq83?72S97FFDN2;(CV(NA?BaPuv(snOZoksbzT2Fwz!y?AZi$Go1p- z=46I=;5;$&+ZgPTqmkHN6#IJW1?i>Aa6}rrre{xxovch%rNh};l#WYh;sD9^7w&1? z?nP$##9Ge*I3=!(hH$YWzgJZbq&v3L&=q3=8jpX!ZYJ|yTDQ{)Sr|j#JETD=w#4xI z93;HWgLc3U8cvn}o^id-VzzQ2w`mZv)0It$wJB=uvbDDBYj%9&80jS=A!+h~ZHs$= z2e+S3sN)&6I@fw_8VZMsZCb}l-OVo0zGqxen2AU0a1nsCTivAUV@3QkH-{mrY9U0O zoEo)mpHBZKsxeiT(Y=ZLo1mmXd69vh-29i>DYwO{HTm{z8plK!Gj|_pD+=dUg?yTG z)Mmd9;DLtidNNQd(rG2H!P3H`A3%)gSbxksR8XKS1|g5>0?TR4<3hKM|3$of@V6UTZtze!JY(CR;bDtV6_%mLlBb8io=O(e>-%Z_ITT^Ip zde=bvX$o>X|M$4oPN*b-b!qe!tkX)Ig3RMA{~i+v7gR(ggwq zTekbzc7c^fk3}qLQG}CbVidKpt5S9U@*s0NxwL>p z?+JC!F8*dy2(2I8U07v@MtkwIz?j{PRlLn{&5-R^B*inGQB%7{FHE1KL1TdlMvQwK zthuz_zlA)rPn|EF?r`=)cQD&QV<|c9;7EKgqnqdt?L*B7PoqL8(yH2CXaEX+QG z|S>2UaL4DGp)`Hn#qY^3uii|+A#o$(>tSAuvm z_~irGgV}XWV#ajt0yH2A6P&Cowj=%0hb2=-ZMYnbG<-t+Vv({kq~&{kLz@u`3E{eu zZsGR!D+nyEH3ZQq1Sx4$ah*myXd;`d+xMm=vC?*& z+qD-nyaQ+6h7@%MKq;JoeEjI$=W6EXe8Mv-m*pjQI3K=J%~zk8$HycTUoln5l8f%I z9WO}P(aYU#FBx|PX+S=;o@p7C9L0u^HZnaIs%+rOOqS!I@Jm&Zvq>hrusk=V=waSd zhElCHScE8p$$%0%IGT0y5aHPNq)s&Ud!i*SXI@m=7PW}|y0Ht?cgfN)wO&*w7bDhk zYdDa~&l};6ArhJ%Vg+Lv`Ag+aG<-$+4|BAkF-AQi1ZLxnew?cVWe-Z+-j&WTTe1l# zAfA(~C4h=3uki6aK)5cof*RWFRlJU=9)IWD)c4rQdW(7*TH0bU(D_~7A1uX6>y_Gj z@F-A2&e0?XrFSdN6*#)Yr8{YNs%CW)5oIs#tWLcqEA8U zJL^Pup_i%|3YV{fI$m%b&aAnA;{?I_#yc*bSrQ@^w&o3eUr{EDan!{OaDgLA4y9c& zmt3wjMy-LUS#*W=u=hH>(d&NhHfo9DqKPxIiiohy{?80D)Ue~m>}KF#&y<^HIBAy2 zLz0!EqM+)4WJI6L7TGk+$tk&%HXKD}OR}B)EUz}Ueo5`i=LjGkZO~2ZyE{nm4)XZ& zZjh1Nq(opH=Ak4V3j*b2gSlefs&mA3^ErY3gP!fmTHYI%v#A$XNdHXRD+GDO6{Krg zrD+0f7=#24X@=DY7N=cpXUs**Squyf;dU^Nx|Xd-;46d>3IMQIon^{K&(NAw z3s6dUIb$IMttJ|1orIx|*Qp{V=6U70>(gTe=liq9fgFXs{wf<@`%bz2Rykd=;joU| zg0UeLRP^VFMPN2iB~_0~cv}W@;9erFw@F0TZJusQ9I*wtLZ97X6sk)8zj|spMe5D8bcpcvZ#aXuJJLoC?N2&IQ-C<8iyoMXyc>2E1Ev4 zX&*igL(ZdtD+AbJp!%K9QoSK7rLJ-YchHqvSulcvv}4}Rgx%sgKn$H8e<%u@d93I@ z#NM$Bo+!9uU~Tp#kQuC8VOF0Ze3)t7Fp^oN%iwc#gU@=&uQ_@E<@L$`Kg9Ejh{yuH z-FoZOP4dnrlVFCs;7MrV14_5;gI%{-%E$79#{+XZskE*-qjd5*)_q_)s=Azp0gDt+ z_ERnMmgAU$<~-19cF&KIGuK%~#4f6umfYpL7wT?nb+mh=sDp^@Aw~YDOSv?w9p)&* z+6yG+s(Mj#Jb50kHSXvkvQ+Aw9v*Y@GP8z5-#w|YjCDp4Ok&YBqo)*;-^hk!h1^4K zTpa7DtTkD{GA5=va7mby|NI~@>M@g~2id;Q@SQ-(@17s0>Tn^zafN5m$!TPfLb2kU zPno@1;bc@RMfx@K;nZ;+k2>gWLCIT6EJ)wQO!-X-gwRrN1ABbR$N3l$dJ?y25shrg zY)=`8c3!QT&KUfzJXg7*g$E_rsWH7lmN^~rMrYaBWEW~U_jJk8W9;bt;0v1v35GA> zaxy8pV4lW4++JSt!F_`YM~NW*6sU;5U=j1&KEcC%BmT4z!Z%&xY&LU8omf^$=Mca} zgy@&qEq5vr^-I~|f`x1E1VA+?Cma&ErnS0gR>7>(jhmK6-cN}}uF(lqS2tILY@*b% zN0S}8CZ(+ApdhgnFl3uCK|-j|A4o5+X`Mw5o550bdobvI52$^pFI&y+Ttmh3IwXzyUA z+JI1LrQaN{&11?ruo0yF;LPATbJ@Wom^p!1fw3XH9%PoVB|Bj(;F<+tyz_%ronEtuRH9!VB6M#298iaj3{$(&XxcdGiKQ` zV}Zb6JPyR3=fLxdm{s(?jeA3)lu)1na_()%Q(*(pr?C6+z~r$n)c9!^O2L6D(SDMNwVYD ziv|IiR)xgcd_ey%PL79H1m>;kzXJ8!P$Gu6e;m%| z8!TNdK)Kxud><=Zk-IJ{$7B!#Z(a@0mSk|T8tXyjB|};iWrx)%x2RU*?nGhPwk7xVryAa zZ+*ldV76}vA8NviaxFMPDRP4~6;xQ?VX~sUrrrqvBkvsCbiFgxq8K{Joj=}aCrR7V z4lG5EjXSBMk5W*egNArDz+AbNwQd=XxeG$U8GKHoa4839CS|HHIw^W* zfHS)sZ6(&xTh}1xE5f>JP`DPQx?)ty>rPARo2>yH&5l^tji^<$sCjAL(D|6c<;%PO z;2YGo5N)!`4a8v+0Uom;BM@js8z>cR8Ud;_f!gY+?G`NV{$j~9oU@}f&zfSiSO+*F zx~$>V4LyaT0DHy~lr$#On9O-G)nd4uH3m`gT^gw0zouKU#lgXMw)8bTNF%xUL!hi8 zJG&hZvwcsn%_BPSlJlL;di)C>_ZBgw>1YIHkKMV`P18L(qsih6y31zK22NVI7-y*i zDv#@2PChAvXOl1kcJr}s!1gf!ll!lI5T^mr`HFOhKul?$-PUZjnq_r-ScH?yC==Nl z5YFb&yO0k!*9pNg!iSzabW2S=Ki04HMkCMELbhFjy9o~K=67#1$Z;e}as|R#OpD{d zlcPL0+3D_7)_kZdc1<};C0caZ={}z7A)m%56crWY>Aw?Bxly7TVRyJ?iAp))oyGhj${u3T~@6pMt;BemYIk zVF>&k1VsyN&F#Z;hiCHyD%jz`T~7x5S(1V^aik=lI*25OC-6;_O@r3+)kh*T2V1}u z7PLO%&qApEg<075`~Uz4s&;a2YHEET-rpgeYtfj2qFn2F zm6rm)I@@WFPt+>oH)mw+fB=FaS1Sv8s#uU?I{C3yK!v^is_MRdDCwG}HgJ1S4b(Eq zM!cGYuyR)RZOBNGZhpHOvI$%_%`OR?FkmmJ1|f2< z&98kwuX$RQw#b#Chn0A7RJHr#Qkr&Q-`>Xlht&PcB6-*| z20pyk%yTbot$H<^U*#-k=h8L%3Sd5EdM2xKhrWSoC?`oa##vnR!8eCgfo{i}zyTw% zY&kaKnbp0~TgZJH&G*9Q+MPQ0XILsHoJ9g9I5GJ%o(*YUjW9@9-dA&~s;n#3K{9dh z>zi8WAj*|&eSMmcGww~1vS;zHHAaJDyCeN-@?jK;BcEAAS$&+bgo7=Rw9X5;dEoCU zG+kRKz}Es#^GK0-Vnn^ILkxgok7UPp^~i6!vo=a=Ok>6-$akc<<0>FO>GrrIRpG1g zp0hLG&1I^!GR6A2pE^E`cEYVgOlZU}WP4=T3vI9SbRLQVWitifhGRYa#tc~vRIN%P z;jP6K;B0@eh-gr{`sYf1?(=yk|v1a_vWrrS0yj1G6ZVj_TVb0gU>&j~0OafKCeWbk@ z6$9$>n+=rlQbpCQhST1Y2#_j)%Q-oRETtVjBAe=S${{zd$KuM?>?HPf6y% zvKA`|rxbe!+bDRN+#V4SjA$$O8Fs!;F$ggCwQ%~vH%49&x<*%3tJ8J5?ezf5%&734 zBZz72Neyj-6dMG+jaeHS?=lrbVEfxB1(pS~<{`YpX>b>u0ErHa7*< zP~!&R353sV2w(4Kmh+LLu}DSZ&~!*|9qwZ~rA9C5cft+LlvH%)M94{Txsh7(Wo_$}b(~q$ zceyNZTeKMPCINX@(?)iWZ<;IEB(4RNat3zAO^8cd?xS)Z(m41o^qAqKyG$l*t8_k@ zkm`OkDz+p>6;`Hf9s7?-^X2S-QwtZJ;WoXoV&Ee7l3@!FI2jOcga2oKrvFEy!SUuZ zSNI-phr4dAq}ILT99MVWNHTF#245lJ#fS2R=SB&#H8IY1RA_UM2!Mz$N3Dr$0cRAC zfy-cdl}(Gd#{<+bD-67jzx)kdAa!jcE|%+{HuugPW~r*L^ZqiE^-h;!G<`_R==!a8 zamxH(iJ6@?dwEFye9r=KS95cgc%L$xpuglC-duO(k}TovM24nK=GJ$6E`NRX{>N>1 z)i-|JFJz!FjIMoa)d&QCk!nG`d6iM}?}g{5h!rV4zNfP>{;I&OfD!#AOu*O>J->tY zxN7BPk65n%meRn$m8&|&Ik-d6jYh9%F6S7;4Am-Qs#H{#ef9=#J^?1RdSr`^LIjR7 zRw3Iv^w#P&sKL}?ix-_HHlL=P_Z)S6S@>!yOkqE%4{o}&-e5jdrWY^LSgq;;`A?Mq zHUJ4?dj%2boLO?9uFim1azC+y}N8NzEZ z@e7&adKgUdSbF`ZrU)$cXF_@Ft6$e8n;Yk%4cxe}j}KqW8`(b9-lZSYr^JeukBj}k z_TKuh>Ggl$1{F{c@Q8$j$nhBDKuTJ$4k0Kox)G2@dbHw12{GuA(u{64N{}ukB}NEr z;;^QIjZSVKBtFPDV`9vDLG`IO$CyCRFgYt1prS`S#eeVcc z#HlZ%&|ho~Y9AaVHkArs7ZfLd5_uGB)jg<*m|688g14MoqVCK=%*7=!`+|24H zvCI+c|NOA~%S2XC->jY+vJ^K-k<9Y^Xq6b*E+Zp{uFf&Ln{AQ?#oW~&NU#wb83!B6 z{tq1ybWiL?epJgO_C74LVr9r={k;5^*L)+8KtiZ!m;Ova>{o(f=E#(hlj=3hm?j- zi&u(<-sUci7%3+d)&Jy`7^up(GWSYEiYCq_mT9S{R1U^KCJH*Ryj@0Q&pUjdSuc9! zNQ6QC%0aRzfdq7_3u&OG(YBFVem4 z`KYU7tGT^S84p+HlJ$3M?!XlB);4hHJ6pl#eCzLuvJOW(w!2^N%mfX_2Y0`0+S~G1 znn&ZO_a2V7JFdchNy(i7B$}8-v}p6&Z-V)Q2EG-!9n+uLTLsX!{?@Gp)3GH!v^c!ruz;c@5>yCRfS6xv&D zAi9VUgZn*XiC&^p3M(3lIjgN^EZfszNwIvZ`Cn0edPj$Y@8 za{yjV8VQT-Gd+PRBlH6}A({sfJZe>^LccSd@9BHe2a+FpuFJdJEt2w(Tj|DdC8P#z z=Z#CR8E%oAWQ1~SYnwU^pmsL_k?6(#J_4i*E4o4g{>=x#UV7VAG^wXF0_KtAd|t6dPn6Vf6id&noO zW1mI9b@u4N43SIuWqX}fq)c65kVaiWWytuUr0r{ma&(mTfHfZ@t~nXo@qBT$q1DbI zANyh=Y~IM{ zl#G;GLkz7RAsM4zLslIso{5(7EE(rtG&0ax@b1lGyBWqa84f->dZt@Ps9&`- z&C2TSjFR+sO&0EV1{)n^@@x5XS6hE_8%^v-vKlO25tZgHt+0I~UzQgbt*_L|?R}$7 zYOY*$UHqTvP4>c8`H(SIK+IMQ}PizWCEj zjZDFU=E@YxW1VVx+2N|kO{yg4^jPLVHT8(qYU%6a2y2aHmby=Q()=u0x^n+UVTN`MJY9uucK{au|7?DtrL1`@^NT zG$OfgQgU~-xXOH*x<&6TN9mD2}ygJ+Fur)z%HT=0p5 z`s{$jtOBGT{XjHLoV{$_XL@C?o?aFe6TJyT|!NpnF8jE?^R;K-E7FI+E1JlbZ zrVZhRq(bA7)k<0)%3@^Y9Hdnb4%f zb8-n5QK=McfMMpLJ1<#Vdgs?i{r)lnk5U~ zr@&6qN0+e5f?q%PQE+NFCn^u$EcwD-exOqAlJR|N&jNh z3)lWJ9z@_LV}bB8)afkOZM}4R!^mVGKZkNpIeyrF#Ykl!yh*2Qe|SnVKf9};g;C$z zy}UFpa3W7+#?|JPX(qD_t(+TkrS5Dd`}tU^WalUM$%0K|IQ_4;mR7iAo{6<=u^APU zQx)#qcy$SXc*P-S=iwzXhR-W081d;!jq5WSTg@pru=EnC%CT@{D1*Ivf<%F0cNG|o zf77WI__Byb5v_f8!R$YbwlMs0#7Mm3}ARxs_ zFXXC@&9D_>-lNP`ht}V|;n5Wr!}58d=(HwwVF5a&GL5`D9z8ZvVpFL$D(Pu+0aU2E zpoBPj6ml}|{4JL!Gf?s@7)2I-w}xG^?Q{J0uXH&f=8v38IPwhGTWh9gJX~n!>h$e3 z?N=(8E@wX#s&?kdi6{tI)r9GF=sX~4Nms` z3AZ@L{x%w*kcLv5*r$KjvegxTTU5C4v8OGCd}_gZ^k){cUUy5+_g9l;_a=+7Sy>UU z*&_trDimlg^{uBrdjl=ZE)cIm3SavJS8YXqby8 zad0dtlK@M4PHz9M@Pc<*&m==GU2z|ZMqnC;y#X4hE}NgDo{(0q4%=p2z;iQQFcSGN zc-?Biy91SI4Hc)&XJr1YkmbIIdr-|4>D$w@iK*~{AT|uj=vczHs~?pyD{GG&Xl%Ad zGvp_hgB07=9syCDu-p|Tw{aie*dD8)k3-4LTB06|mQZrr8lOJH#+u0<-ER-sszc)7 zNxbDhdWL%4Ab)?LL20r(TIs`REA1LbSV6Fwrr&C9G1?!EzFN&-z9*Rb;vHI>mn)LR zp^I``4JltXfqT-y+3*B2WPR51z)f%X(Jh;Q360b9E3@!Pb!#r7wtua)$+u|)AJ<;&gU!fmc_fb6gC!t-hv0X`$us1#$uRe`H=wjdc#k0Q&= zcIK%fH---Zoe?!jc`}GezNgyL&ahJz)_-(P)HvMbu!_IoE|AISjm=y0JCw{@TKBmH zC~m41``&^%yGx}Ojk5z>6l{Uz$2#h2OH>8YMrb1?I#Tg4M)I#%K^>TQNyVR#F z<*WErfr)DK9pZpZf)uwXXRfzYu+=Q{Pt=d+J@N+|lcarwTT(S`kqx#Klv*zgW}<0b zBG-#MI5}LW*K!ysY*_oM%6K|F{d5OP+-2+3RO427J`>Y~9Cx~AZ?yZ_)a zzDLjErSGljfucC(YcbkdV?Up-6xduz!PZDHH|l_l>M*M)=$OBA{X8jp=RkT$5TEDP zOv{Xp8cbwfKd-{MbH_?+?fjiMB~^&9trc;)kUP9eR2izh{OzR86e^3t+VpBSvf- zbt~)*cJ6>dK)kou;YE-+&7H`Jsv-jKjP08oYMk~Bl?5%Xg+$}^GpofDSB=%{xq}w@ zSjeFh#oH$-6YPp@S8lZq5 zR=!b`RIcHeVS#z|@Mli9*)*cc-Jo+xqOSZpi0VCTxqV6%P9bfQVTnxX-YO3Kf^pM- zixYi~%q(~vgQWbr{vyKo?{2PO!c*vWjN!w2ZYnpxbD@4|d^dMM3B?woUn*CptvwW6 z5}deRsF6?5G~V8aqO>&G>Qq!6MB@rlG1%LFD?vtC+o{8P7PAsHpW>cc*A~>(Mlo`w;pFWJVOyJHj3DWf#bpp1-QHV!!OHzctgg8TCHa@hARhU z#`e=kyZHOk8cYkFv$zi~D6HRw-xxF`1{6Cv9_-bByjO3*9CahS&g^+DQVOrk}!v z>viH0>t9G+MH_@Vx@&wuLN17izFZ7*%fPm*C+(J4Ejgl-*7s}ux?%76D#~+V9PJeR z2Y_7@1@^a^g_ZMmd+e^<(C$yHb1I)%B^Khd%kq^&43uqqgt}MSmUn~Yo&NHT7e>ag z>2!SP3&GNpIT|hskPVru_|H2m4MlAVl1bm32;g+p&Nvh)@)gX0KhdECL^W$!Yr^1Q zbF-sbw~Ma;LmuqM)bzqM@)90fo^IjDO<>ElIF%w^MX{K}sf4c624G+9)m`2D=eTCz zbpx!!J^aX`pLbrvH&}7r;B-ET_j8m?cgnAZJzE1GuB#LD{`+pzJGq~lKk+8ezWDfW zt96wPJRW7f?QVnkDqWG}5kFEPL@8v$pJmm7@*w)kwGMUS^$4l}`03LF!mq&gQw``_ zQ_pMDNF-ngfs^YlH!Ao$2CN{?v;t6LXT~@Kd%J9NbV?XUGmCQ$tTSvguFNjP=vs%w zUNT$!Aqgn*0Go=1kd<>1{qqh|EhycQujAD{V-!jz+0_sQk@}dGyH1mI#^-V_UJuF z%lbtCo8^Hdm3rwxXA9Bq<;2N*MdqDHGj6}v8&|;Jp1`)}@X4mFu7UvET?Z_*yqrQCD9?PtBM0lt_lVLA2Nl-88q32V+Z^RXYBv+L zB{MHHE|2{}FVVjG(B}-Lk)VA_O>|Ly3pLPgVFBlw3$k;~e&QYOjBYuP1`l15z>;vK z+0!AfmnX?IF<~X_u_u9Wb*ZmuTiW=;VZ1V}n9Ot`TTq=`+Kb;T_f&O>xPx@1db6wR z*Y0V}Uv?X;tz*l@QBV8FtRr}FYsDcC`L<=XuFr^wp7s%P%;L&anjXZ_t_~+n{b7T^ zPC{VS501@+PkpRjE~T=e=twLEJ~WdxxQUBS8BG@c_Us2XLp*_}( zHnAKUifP*zJx#U3Y(L%gXjl+sTW(kI!u-sXs>QlcVB~#pjMJ%O?F+88CQ$OsYL<#m zcb6_TU0Bf6UuYhcxb$SpQ@$#3=(&GcWKWMzVt3EbP<+e!y00}cl85+^>4m3t$$qij zI%Uar&7~Y!M5dUJ>`F6P-_03Uy3DVc?CrOvLOqT1hy(+w>eH_oF~ zIyk?}{(C|rYX8?aIxHiGk(svDeX9sE2u_FGyC!I1NOlO_5d5%IRP_H zEaEP)AW_Sowfhy*vz{}tgqh#cr9-vR;Zyh+zeA#yOM?oThUk}btBs1KIW(qEh<&Wk zPy*WnaEd!Tr#BF}6;-2pm)`n_df=%vD!KgAD5Aj^&<__CJiyV05@RH}G8tQ6e(?DVL6PiNa3Dex?i#p6bcuO(g(nvJazVgV;fdxG z1r-c&Ob_1yTg~5|>`ixPYW5aujkwu@OQ|7;Mw2Dy@I(gNyi3uu-q6nei;PWgAW;t* zTAC}B+RkuAG`G<#HHW$}!k8~kW>j#<5DmRW)}0BM1vSVY?PAId1y^FvX_vg+mUdV- z9Zv5{z==bva#IcDkhvVLJxY3m`(H=kRnQl#!FXNa0^4XAfm?7fE3zDpK8dSdKK*07 zXhB5<)zhD;h9y*P(?@pZ-1Yr!cXoj&=DDurxos=kCQE~4Nft&##B$L0oQXl*e5wA< zdRz-Xd_lwLLTk&2v?sBLiZX=k86e#VMVB~Gs!iAmyM7DO*SL=J#DQf2t6QQwK_!4weM$Zx>Whbp_H zrw_}oU}Mt>#k?ss7AaSNgzULR*gT_;u9o+7svIwV$cy-au zcmyzt05qL7)@KT#-5*=g;4N@glqRq9G+!oTN+RvO*#f+P2z^~=Ez3vz8=^?1>tXD~ zd{^JZx-~@xK4z{KXEL@zo-x5IWX(%eKYSGYNvBuKDd5dj(T?cZWQt@eE=sY$bQ1Ij zK_!w0xG&~ zANDzB)Pbi+D~=b6i$m z@ROc5DTJZE)s7{k=Ei(&V!?$?A3+{}H;+QAhNs+hPz>1$ zxpsw9G%4CJCPJsSvBA0R`{&OUMz_EFxcN`wPwmQKEcQ-0(lv$fS7Y1oG2I^5RM$+Y zrw-f4UCbB~)5>9O0fdT1b%zVd;^ABFwR8Ch8d@bBebBhkbQd`G?ZX_r;f35MQ8)Q_ z$))Pn*hygY1|U91*fw9{+P141VH>Iu{ihPEy=hDWbu3ctnZjq zzqZdeEj&!`$vs5Xt+*43HQMgppN(bL*o!Kw$(()I>+}aD(9Rea2atDXF6nL)Dje!! zyi7vBw)}J_7Ht*By8EE)kgj2g^Y~?P zv^P2`u<17Lme#XbA~vNbOrVh^smLaQtt>ws^CD`cqee@{_o?x;Q$}{UizQEP+DiAjQaKo3wr-_->PE`@RE}p0P!5;!BaI$tEQr1)X=; zCD==fdBp8?5FiY~1|XQYLUSw(^R&49Ng;0bhy3h8Bczx?vs>Elmi6Jbosu0P^ROu| zS$QcLiEkfAV-ZdX>O|8_O^ib1N{2~Yc1E9upgtu z7AB{-Pk5A`)%$C2Y$ejPw+4-Bt<*}PM0vUrShH?+pik11Z7YfPBBR~eA%6SC7*;L- z(0+I=uW(1r0jfPkTh;iq^g8mdYLH^hNn_Y?8{w*>KxRFcQW3pS<=f7}#AX#o{h5dn zwRF!*5nngY_N|jD&JdPIG&CCgcrx&J@|%b_8}pCT#t1l%;OxRj-@4iHeIlhCHU{Dc ztqeb>U6ALa(#50O2J&TX&X#vFP zF=vZua&6bd+KsE)=52L>7pqMbu`ZZM9G_WbB=26742ulu5sX$xCvTit|F&4YG|w}N z5Z#wM`##M^EpM_5{vzXib@6?cvx?i09pjwQ-<-SK*WdaG zM1(h|SAP>K9b7KK6}Wxh-$U0ecI|cMdi5>1PLC~+*RhO9fq+Xj0K^TzbxwG(fB*I` zH8JSP4T)wD3OtM`(p=6SCYyZl-8}l4|5k zjw)&M4Wc&bj!r!%HQTm{U+nOKu#bF`n=X8xQrR!oaVV8}Cjnl_Qp>HsuUBejx=@p| zK+wAg$q&UAaCh2Y{poM$e^G@+YS%tK;n zk&*d-7qLETu}JSS5QaD1ZIq8tRNXIco(o0V^qf6?#}~m7#OP0#3UZ1|d@!)x<qR2=N&6BZoK z-4v;L8MWc={bWUVG7m)YqLNS*~{Vwy?bNmbqL zW9nxHeUuYwp|!zoT*6~wr#IC()GD{@!xhO&b52!!t7YjIT6b#0XX~!hv_y z!ozK+S+l;CnTMfkz%l^``<~Gl4PQsofbBGx6pCU9aP^AYU;9&gwCGr0Q~O5TTd$PK zxcbQG41T>j#&*z|+=bE7G!tZRTY=bhiJ!iu1ZuKbA08Ty-4ew^MK~fDpV(_#pSxS- zE75E_ciRxvPsdeXd#NaUk8*=ILVR+UZPXR_ukM@<${927pDVd5wO`&^HKGsC_K}@= zVK~eIA2+@~@5gDbGZvpjB|}-Q<>?Ui)%quQ5It&|f1)60O(n5~mcckCDa-;@XFhCb zKomxVRcWPxiRPvMz9`7xcv(HNR}%2wlUsvRfy zs`UGIacn8HoNlQtl8&8j^r7n&W+Q=I(XmPUd}g0}8V#=OzjFM9Q%nfM8?aBLEgcCz zI7C61{63#EAAoZ~c*5M|B@fx(hoh#izG1`Q%@`?ANe`&I#LW}aR%WZ`^2v+d8Pjq9 z^t@6v=HX%FQ#}V0+A=q#KJwUzi4LK?0sCyXcXRAw>RxIv!I$gc!@a=8OZ~Bi%KrA{ z5XstBhC0j&&d(v}SG5FUxM#1js?T0P@crfdVwUfQkF55l-rXR{|S+*a%9`=G53AtQ9quPPS_?BF_cO=99b zI&IOZAa+%@#T5s!n{{*2I*+&G!{-$p62`*95Bhw2_u~#{1AY~LS`_xC<<7L%~!V!ZU@Zw|RX{|)=H!2TIJ|8<3a zyen`|jL5>7ujWWq+~iJe_JQLtu8tb#mQ8a!b_le`fp1`smJ$0s;-GBOqs(g6AAW%Au@ z+vtz@hC>vnz5LH{*>}mRteM*Vq9;M>iB`L0r)PGWJ^-`Q_7^E8IRJ$Ir2d<*fInb9 zkrU|%AJO5@u6co�WrFz#pv34;u!9Ks3@+@{e%CsbCb7c=!QTz~Yb|QR@v-ht-Y< z@p}`LEbrNG!In2c^2wti$nkw;71*@SuW3sW6{tN(!m&q?l7aHBa4>@|?~tu;pIy|> zk69mC)IL9?3XW_JcQ+>-PHZ7$0n|D0cxQ@C{3z^|vWMn<@;6ttofc{n=653RJ+VjN z0aq}wqWFaD?WKKvW`m?E$fgCIiEu0x#jk{7n!<|`xe>f~A~C{kLj^1dmwm_M8w)5w zUUZfOqCG$RkWB61A?Q7FEpD-{2l_qsxB#pF)Ja~NAm*Dy^A||2G7=+;f&fKgo(oBU z)o}te&q`Kp<<5AJ6Pq=2CRCF*?HG4Y#@K)EKW84<2H=_>fAi7r7J&QqK(Cu4YUTJp z?}opQC-dqfjU`8~yA+WMs%s52`$8FY|AW^DfL(wH5Ww^};0MXQ1U-S52asPjL8ERx zBg<{or!n&P>&ri(pz~xgI_o`4F>XDl^DaX%x&sS0z{p%UsZRmaJn;B9IMYs9=qarf z`@XVbm;)fDTbv;I=X4`K^nuN18i6gy$ZhJ$J+rg^0tg(Fxz~S|w17nDIf)uTkbf1j zzr0yYUZ}!MualAWpIK5K5ybd_h}$$fg}*>Ghx zA>%}7DCo$#uaQ(^FfQOtNd7;!!9TOHHF}f29Si@)!bHtZ7O!S-RVS7SHGaaCFVF^; zkgQ>t3YrQ>C1CDDkU{r>hKk>{Mv?n}(zv5)8BC)s{!YliNW_!OiRs9jNn@k=U$F+~ zyaCEM8mPFgv?tlBWr)^$G-_Avzt)ND%s&q~qew4f}uN-a(jo zRe$%RfqSY)*m@Ao5{02r%=UHEVx&%fL?S0Ms#j{bLw+4)&P3X0322L}aQkYz&$q~n zcmM9a^QVCZxCsq@&=_!c)6I4&o@_8JF%%=)`P|TAv1q1VX<36(W0uLzIV)tVS{0ur#=@Q9YY33QO`C5sJY5>4q-Lq}cp<2TMl! z>v&7~019Rcb1)fs902b`mhlZ5m!iD@$2NzrtYA5ASn>cGa-zHWGFysVOuJCajbJX@ zeD2uNaILVAMU9E>v5^;A<`$w*le;uzTn&~W=A&8u=PLrpb@f`UK@4b;x~IB3EkXi@q9yvgKl6q9rNOJJ6*oG+AA2##p6f$M2dBb7?EYNkkjfqSNXP=;A~s#@pS&{0LDM( zOQTZP`>&VqCo02$Hhr5`>p`=d{90Yy%}zj^Sp{4@-D$6{G1t1MU4p(ayC7iS{i-o& zU1sN>)?>q1V@dx9jVOin$HNBS%C&CS!vIn}3HRaIa7`BA4)0P~uxasByabUWjlc^> z_{$f>ik}z>DW3s23e4?wsT2G)%P2@dn=G?TX$7(y42?4P#t&x(uXU1hm4x6rli<~V zW?g zKnrAC7Q|`%-XlrdfLYA{!)U)7N$AI%8O(4mDo~Zg9`rz*l4OoRn$B+dSJ9fB$h|VS zn>s9PHO;nbUBkD^E6nWtc+eqtI%&{M*IzQvin#&mRWv3H$@{@@FtTDFypOWF9vNQ@Ak9^Is+^2B z%R%^&lm(l~jf!lVl>PiAu$_l>AAbF{B!d`3+8s6LkMdWN|omXVVI#dZja8)6>14$;YVzDvX-Y8+eBa5C?&T#GwqTm?M^g zca?m)(;+zV3bLSo$bGZ#o~JFZ*m7CFfL~9-8|;xT3Hs{KH^gMbD@uTPb$THKUd|t> zAVMD^<5JrDL1T;bGq8Zs|BmWxoq+lBJ@07!^ z;gjkEko@hZKv(~KhR0!9(FJS`@N)8}wX9x0ez!8=xuZAlH>bR%-ikkBlm?Db0W%ON zw*(^AEr@vq{$kT&Ej{9bo*oyLYO2(tEL0LTJLmx?| zoYp_ahX%E!ySn9XLAREcu7oS6Rq2)Ij+-o*Cl4_A0FZ!tl!G8wCZD#dBKHQ9z+0iQ zT~a9gQ}>tWc$T@nX+*@>mb=(deCXrby_*Si4aHR#1F0n}fE>E6HBz%3-!Sq=i|EDQ zaAuRkiCSpP2)gp*_#Hc|qo%{jkGSCoq>&n@K`Xa%%6{SQ)P}m^biNFk{UOH*I1&02C054 zsN=g7a5y>XFGKRvq4rRgLo+Nz|Q30R{Oct1OO@RZIEGRy7#d`c_&Z10}Z zdCy9o$+bay?Db4GChherP!GO>{A1-dLJYd()J|#G#!Zqf&ys$#+CDdlpMs^vl&j7q zi}f#DGDnxhlVds-x*8&mZ$YKYCr`qUS)tlU_Lmze&SAX^be~&VAF|W}=_M?V=Dfh2 z<(vOaQ+DA2{;Q{($6-h@&G{#J$>z0pjE6CYg-a)8_cVO#z)$AQ*7T)@&DxTisbU4L zZa?=84mcEcCxNAEYLD}L-z^1I-MnNC9Efz<74Hxeyt-ZOs21nVcuJMt#kzqsKwtTC zd~5afB(@-!{Q^ZQI9A#J%QN6>o8riYkrgj`G0BPJZ#8j^Blun->G#qu?G&IcHI_|J zV_OVFGki|vexC;1-KL*|&!29m4L^6JzH|JZ`pVFpPkq^Xj0w(UUv}N4g0itmWF1Gd zYEqqbaPdA+^ard`5D&i!dg-dnTPHu*->K&%EW+zX+PE^u11|`3kAZgSk6t2I{=l0J za&Bu3bn|@r*~m}UD=Za1-ozKT%V!3GtKAB*f_IcGMJceU%Mz)flb;_KIv7+ zGR!Z6whRDb?x9@TZaK4p=~yiIU1e|W)>917-S}Tgl&XL-=I5G=KuTgrW|8b1hWH7v zm8a-ZfF^*Ba1+Q~x*mFK{GU%&2cLYDvNoFGE%c^iHkFmmJhbAE;K+AU^vFv+msJHV z%X=2p&`64eQ+Rt)e*B+7Hbj+PC#g!!CTq}Xj;W>890~D!PE}9-+2O}h+v=~Y@yvI( zfcHNBuPkE{6+u}Z*>6QAp!OjHV1D=R@oxf2Q`Yh5Zf1O-2g+sj;dWNWu{GtS8huj{ zn~9iZ(gl#3n(J^rIt?2zM3Brv^tNC+)y<5l2uEsbET&yTtvehh|EIQO5SjnbmklPL zRdJnp#rn}q?zu&WB$Fn0x)jC|Gw7ON@PPj{_TE>8<1dP-Ejue=A>s3*e17~kPe*5( z6A^T#vqzG7HIw2i%lrFYpc>SwKZ$MN_@5ojgfJl-6*6cD z(}*`=*BxAjaX*$x)0OpOKP8}Q7rBF&Xs|kUzBAq3J2K?Vbpj3r8< zpgCD+yLmOsD|J~l)_2

TBTA@g$M=YsOk+Di5yA7)e*7_?}=;x4EChv zWC80*!Ie4WxwqM!*Z-zRTW28SUwZZ62?vLABE{XgM(cqL{Lh1|QNzc|DDb^@Vk3wn zgPqUG9tQAOXNvzm>WQbZsui)Lo_YPbD2?+(4V&75?vVBN7Q&CGE?^P&)MLRI!dcL{ ze{Yqy zKRc=Hx@RR4NLU_&AM@P{`afkJO}@i{&|dC+(R3$sofaF1n##3wrT;F6os}@WYrX4+ z9NqIYG0n7N@(g~wH}%Ks82Zk8Bv{~e9UWkvW&@+6eak<`-{J&(@ifO)WLQNkQi@hp zX-bhuBp7wKz+6|8MenbwfnD=S>_6vxw2Ikh5zz*|@k%tDwpl3R!aHkvYpP8~S)J#a``- zjbnv@+GtPuD$c>UyVdndO-LrbqKYPn zOC(4SQ!`RMJEo7(MvQjvs}UKN${(>k_tK&h_l1$_?Xi%Pj7;;|OzC}+w$W$LwuD3d zcjYL@W8Pw6$@5$sSa%QZ*M}XENg(~#n?PDdbjT&;?s>~ddM)bprFGaN@*neoRE&+Q z>0uCx(UV)Vxah3+@lOv{#;GQJcD=f9L2Z3mmm5>N?(pMJtOsWTc|Rlch5-vNh%4i!#mexLevP7Y zYU-l~g$kOxK~IMJzV~Vf1;u4(SugIPCLVYg_>};JwLh>C_D!6$v-;w5611(L&Mvd` zJ;D@N5C#%hGWv2JKkoA@q06H@9g(01R+8ihW6yK@wlSpft#e_r($ zz~_wh9R92YR)Q98INQkZ59AmtP;h0G*Z7m9Z$2lMcfmUfs@DidJJ;zO+AnVks1%;u z4GR}n=0{0dX*a|W8p6NqDf;z9{An`tI{t>qU@sns-bbs)_a7E?$ywy3JIRSmQT^{C zYH$%S2XASe?b^gM?+#sRaI;??d`Nd&=B?1dF32IeQth%k*|ukX^0TagFoa4Hn&OIr zw+z_Q!4DpL9SA|1Dh3$q2)CDKMs zV&k9Rni_N>O~XU}>Z5@71$6GS|25L@e>wjO`!ll7<=L)_c})2${Hw2rF>Ey@mPn~^ z9|%r3&{4!4KNHzF_--}l)QJN3KHod`wA z_<%!{M-?e2`WO|jKQY^bU0$)&pSFJ~Zp$B?D9vEDAW}m1`RYmceV0Q)HMgJiL%Rd! z_3XCMuEFnl;-Du)@9IBptm Date: Tue, 21 Sep 2021 10:52:13 +0200 Subject: [PATCH 0051/1608] feat: introduce JUnit5 extension (#545) --- operator-framework-core/pom.xml | 10 - .../io/javaoperatorsdk/operator/Operator.java | 7 + .../config/AbstractConfigurationService.java | 12 +- .../api/config/BaseConfigurationService.java | 28 ++ .../operator/api/config/Utils.java | 12 +- .../operator/api/config/Version.java | 3 + operator-framework-junit5/pom.xml | 46 +++ .../operator/junit/HasKubernetesClient.java | 7 + .../operator/junit/KubernetesClientAware.java | 7 + .../operator/junit/OperatorExtension.java | 275 ++++++++++++++++++ operator-framework/pom.xml | 6 + .../runtime/DefaultConfigurationService.java | 6 +- .../operator/ConcurrencyIT.java | 173 +++++------ .../operator/ControllerExecutionIT.java | 71 ++--- .../operator/EventSourceIT.java | 62 ++-- .../operator/IntegrationTestSupport.java | 198 ------------- .../io/javaoperatorsdk/operator/RetryIT.java | 62 ++-- .../operator/SubResourceUpdateIT.java | 140 ++++----- .../operator/UpdatingResAndSubResIT.java | 97 +++--- .../DefaultConfigurationServiceTest.java | 11 +- ...bleUpdateTestCustomResourceController.java | 2 +- ...entSourceTestCustomResourceController.java | 2 +- .../RetryTestCustomResourceController.java | 2 +- .../simple/TestCustomResourceController.java | 35 ++- ...bResourceTestCustomResourceController.java | 2 +- .../TestExecutionInfoProvider.java | 2 +- .../operator/{ => support}/TestUtils.java | 23 +- pom.xml | 4 + 28 files changed, 717 insertions(+), 588 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java create mode 100644 operator-framework-junit5/pom.xml create mode 100644 operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/HasKubernetesClient.java create mode 100644 operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/KubernetesClientAware.java create mode 100644 operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java rename operator-framework/src/test/java/io/javaoperatorsdk/operator/{ => support}/TestExecutionInfoProvider.java (64%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/{ => support}/TestUtils.java (60%) diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 8d0515e545..5449e14728 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -88,16 +88,6 @@ test - - org.apache.logging.log4j - log4j-slf4j-impl - test - - - org.apache.logging.log4j - log4j-core - test - io.fabric8 kubernetes-server-mock 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 c3a2360be4..b2dfef58dd 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 @@ -3,6 +3,7 @@ import java.io.Closeable; import java.io.IOException; import java.net.ConnectException; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.locks.ReentrantLock; @@ -57,6 +58,10 @@ public ConfigurationService getConfigurationService() { return configurationService; } + public List getControllers() { + return Collections.unmodifiableList(controllers.controllers); + } + /** * Finishes the operator startup process. This is mostly used in injection-aware applications * where there is no obvious entrypoint to the application which can trigger the injection process @@ -99,6 +104,8 @@ public void close() { "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); controllers.close(); + + k8sClient.close(); } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index 6560ef9e55..e85a4e8c4b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -7,14 +7,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public abstract class AbstractConfigurationService implements ConfigurationService { - - public static final String LOGGER_NAME = "Default ConfigurationService implementation"; - protected static final Logger log = LoggerFactory.getLogger(LOGGER_NAME); - private final Map configurations = new ConcurrentHashMap<>(); private final Version version; @@ -60,12 +54,14 @@ public ControllerConfiguration getConfigurationFor final var key = keyFor(controller); final var configuration = configurations.get(key); if (configuration == null) { - log.warn( - "Configuration for controller '{}' was not found. {}", key, getControllersNameMessage()); + logMissingControllerWarning(key, getControllersNameMessage()); } return configuration; } + protected abstract void logMissingControllerWarning(String controllerKey, + String controllersNameMessage); + private String getControllersNameMessage() { return "Known controllers: " + getKnownControllerNames().stream().reduce((s, s2) -> s + ", " + s2).orElse("None") 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 new file mode 100644 index 0000000000..5715d707a0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.api.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BaseConfigurationService extends AbstractConfigurationService { + + private static final String LOGGER_NAME = "Default ConfigurationService implementation"; + private static final Logger logger = LoggerFactory.getLogger(LOGGER_NAME); + + public BaseConfigurationService(Version version) { + super(version); + } + + @Override + protected void logMissingControllerWarning(String controllerKey, String controllersNameMessage) { + logger.warn("Configuration for controller '{}' was not found. {}", controllerKey, + controllersNameMessage); + } + + public String getLoggerName() { + return LOGGER_NAME; + } + + protected Logger getLogger() { + return logger; + } +} 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 83a537cedc..ec1b6e3d72 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 @@ -37,10 +37,14 @@ public static Version loadFromProperties() { Date builtTime; try { - builtTime = - // RFC 822 date is the default format used by git-commit-id-plugin - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .parse(properties.getProperty("git.build.time")); + String time = properties.getProperty("git.build.time"); + if (time != null) { + builtTime = + // RFC 822 date is the default format used by git-commit-id-plugin + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(time); + } else { + builtTime = Date.from(Instant.EPOCH); + } } catch (Exception e) { log.debug("Couldn't parse git.build.time property", e); builtTime = Date.from(Instant.EPOCH); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Version.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Version.java index b939f8e384..6bfb5bb2e5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Version.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Version.java @@ -1,10 +1,13 @@ package io.javaoperatorsdk.operator.api.config; +import java.time.Instant; import java.util.Date; /** A class encapsulating the version information associated with this SDK instance. */ public class Version { + public static final Version UNKNOWN = new Version("unknown", "unknown", Date.from(Instant.EPOCH)); + private final String sdk; private final String commit; private final Date builtTime; diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml new file mode 100644 index 0000000000..926abceadc --- /dev/null +++ b/operator-framework-junit5/pom.xml @@ -0,0 +1,46 @@ + + + + java-operator-sdk + io.javaoperatorsdk + 1.9.7-SNAPSHOT + + 4.0.0 + + operator-framework-junit-5 + Operator SDK - Framework - JUnit 5 extension + + + 11 + 11 + + + + + io.javaoperatorsdk + operator-framework-core + ${project.version} + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.jupiter + junit-jupiter-engine + + + org.assertj + assertj-core + 3.20.2 + + + org.awaitility + awaitility + 4.1.0 + + + + \ No newline at end of file diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/HasKubernetesClient.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/HasKubernetesClient.java new file mode 100644 index 0000000000..d93032333f --- /dev/null +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/HasKubernetesClient.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.junit; + +import io.fabric8.kubernetes.client.KubernetesClient; + +public interface HasKubernetesClient { + KubernetesClient getKubernetesClient(); +} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/KubernetesClientAware.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/KubernetesClientAware.java new file mode 100644 index 0000000000..8a1a702074 --- /dev/null +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/KubernetesClientAware.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.junit; + +import io.fabric8.kubernetes.client.KubernetesClient; + +public interface KubernetesClientAware extends HasKubernetesClient { + void setKubernetesClient(KubernetesClient kubernetesClient); +} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java new file mode 100644 index 0000000000..3a7ce8b0d2 --- /dev/null +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -0,0 +1,275 @@ +package io.javaoperatorsdk.operator.junit; + +import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.fabric8.kubernetes.client.utils.Utils; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.Version; +import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.retry.Retry; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OperatorExtension + implements HasKubernetesClient, + BeforeAllCallback, + BeforeEachCallback, + AfterAllCallback, + AfterEachCallback { + + private static final Logger LOGGER = LoggerFactory.getLogger(OperatorExtension.class); + + private final KubernetesClient kubernetesClient; + private final ConfigurationService configurationService; + private final Operator operator; + private final List controllers; + private final boolean preserveNamespaceOnError; + private final boolean waitForNamespaceDeletion; + + private String namespace; + + private OperatorExtension( + ConfigurationService configurationService, + List controllers, + boolean preserveNamespaceOnError, + boolean waitForNamespaceDeletion) { + + this.kubernetesClient = new DefaultKubernetesClient(); + this.configurationService = configurationService; + this.controllers = controllers; + this.operator = new Operator(this.kubernetesClient, this.configurationService); + this.preserveNamespaceOnError = preserveNamespaceOnError; + this.waitForNamespaceDeletion = waitForNamespaceDeletion; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + before(context); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + before(context); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + after(context); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + after(context); + } + + @Override + public KubernetesClient getKubernetesClient() { + return kubernetesClient; + } + + public String getNamespace() { + return namespace; + } + + @SuppressWarnings({"rawtypes"}) + public List getControllers() { + return operator.getControllers().stream() + .map(ConfiguredController::getController) + .collect(Collectors.toUnmodifiableList()); + } + + @SuppressWarnings({"rawtypes"}) + public T getControllerOfType(Class type) { + return operator.getControllers().stream() + .map(ConfiguredController::getController) + .filter(type::isInstance) + .map(type::cast) + .findFirst() + .orElseThrow( + () -> new IllegalArgumentException("Unable to find a controller of type: " + type)); + } + + public NonNamespaceOperation, Resource> resources( + Class type) { + return kubernetesClient.resources(type).inNamespace(namespace); + } + + public T getNamedResource(Class type, String name) { + return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); + } + + public T create(Class type, T resource) { + return kubernetesClient.resources(type).inNamespace(namespace).create(resource); + } + + @SuppressWarnings("unchecked") + protected void before(ExtensionContext context) { + namespace = context.getRequiredTestClass().getSimpleName(); + namespace += "-"; + namespace += context.getRequiredTestMethod().getName(); + namespace = KubernetesResourceUtil.sanitizeName(namespace).toLowerCase(Locale.US); + namespace = namespace.substring(0, Math.min(namespace.length(), 63)); + + LOGGER.info("Initializing integration test in namespace {}", namespace); + + kubernetesClient + .namespaces() + .create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); + + + for (var ref : controllers) { + final var config = configurationService.getConfigurationFor(ref.controller); + final var oconfig = override(config).settingNamespace(namespace); + final var path = "/META-INF/fabric8/" + config.getCRDName() + "-v1.yml"; + + if (ref.retry != null) { + oconfig.withRetry(ref.retry); + } + + try (InputStream is = getClass().getResourceAsStream(path)) { + kubernetesClient.load(is).createOrReplace(); + } catch (Exception ex) { + throw new IllegalStateException("Cannot apply CRD yaml: " + path, ex); + } + + if (ref.controller instanceof KubernetesClientAware) { + ((KubernetesClientAware) ref.controller).setKubernetesClient(kubernetesClient); + } + + + this.operator.register(ref.controller, oconfig.build()); + } + + this.operator.start(); + } + + protected void after(ExtensionContext context) { + if (namespace != null) { + if (preserveNamespaceOnError && context.getExecutionException().isPresent()) { + LOGGER.info("Preserving namespace {}", namespace); + } else { + LOGGER.info("Deleting namespace {} and stopping operator", namespace); + kubernetesClient.namespaces().withName(namespace).delete(); + if (waitForNamespaceDeletion) { + LOGGER.info("Waiting for namespace {} to be deleted", namespace); + Awaitility.await("namespace deleted") + .pollInterval(50, TimeUnit.MILLISECONDS) + .atMost(60, TimeUnit.SECONDS) + .until(() -> kubernetesClient.namespaces().withName(namespace).get() == null); + } + } + } + + try { + this.operator.close(); + } catch (Exception e) { + // ignored + } + } + + @SuppressWarnings("rawtypes") + public static class Builder { + private final List controllers; + private ConfigurationService configurationService; + private boolean preserveNamespaceOnError; + private boolean waitForNamespaceDeletion; + + protected Builder() { + this.configurationService = new BaseConfigurationService(Version.UNKNOWN); + this.controllers = new ArrayList<>(); + + this.preserveNamespaceOnError = Utils.getSystemPropertyOrEnvVar( + "josdk.it.preserveNamespaceOnError", + false); + + this.waitForNamespaceDeletion = Utils.getSystemPropertyOrEnvVar( + "josdk.it.waitForNamespaceDeletion", + true); + } + + public Builder preserveNamespaceOnError(boolean value) { + this.preserveNamespaceOnError = value; + return this; + } + + public Builder waitForNamespaceDeletion(boolean value) { + this.waitForNamespaceDeletion = value; + return this; + } + + public Builder withConfigurationService(ConfigurationService value) { + configurationService = value; + return this; + } + + @SuppressWarnings("rawtypes") + public Builder withController(ResourceController value) { + controllers.add(new ControllerSpec(value, null)); + return this; + } + + @SuppressWarnings("rawtypes") + public Builder withController(ResourceController value, Retry retry) { + controllers.add(new ControllerSpec(value, retry)); + return this; + } + + @SuppressWarnings("rawtypes") + public Builder withController(Class value) { + try { + controllers.add(new ControllerSpec(value.getConstructor().newInstance(), null)); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this; + } + + public OperatorExtension build() { + return new OperatorExtension( + configurationService, + controllers, + preserveNamespaceOnError, + waitForNamespaceDeletion); + } + } + + @SuppressWarnings("rawtypes") + private static class ControllerSpec { + final ResourceController controller; + final Retry retry; + + public ControllerSpec( + ResourceController controller, + Retry retry) { + this.controller = controller; + this.retry = retry; + } + } +} diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 1e0a3f826e..90dff4f18d 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -79,6 +79,12 @@ test-jar test + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + \ No newline at end of file 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 590b9fb746..099754acd1 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 @@ -2,11 +2,11 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.config.AbstractConfigurationService; +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Utils; -public class DefaultConfigurationService extends AbstractConfigurationService { +public class DefaultConfigurationService extends BaseConfigurationService { private static final DefaultConfigurationService instance = new DefaultConfigurationService(); @@ -32,7 +32,7 @@ ControllerConfiguration getConfigurationFor( // create the configuration on demand and register it config = new AnnotationConfiguration<>(controller); register(config); - log.info( + getLogger().info( "Created configuration for controller {} with name {}", controller.getClass().getName(), config.getName()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index f8157c648b..bf06bc054a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -4,129 +4,98 @@ import static org.awaitility.Awaitility.await; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; +import io.javaoperatorsdk.operator.support.TestUtils; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ConcurrencyIT { - public static final int NUMBER_OF_RESOURCES_CREATED = 50; public static final int NUMBER_OF_RESOURCES_DELETED = 30; public static final int NUMBER_OF_RESOURCES_UPDATED = 20; - private static final Logger log = LoggerFactory.getLogger(ConcurrencyIT.class); public static final String UPDATED_SUFFIX = "_updated"; - private IntegrationTestSupport integrationTest = new IntegrationTestSupport(); - - @BeforeAll - public void setup() { - KubernetesClient k8sClient = new DefaultKubernetesClient(); - integrationTest.initialize(k8sClient, new TestCustomResourceController(k8sClient, true)); - } + private static final Logger log = LoggerFactory.getLogger(ConcurrencyIT.class); - @BeforeEach - public void cleanup() { - integrationTest.cleanup(); - } + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withController(new TestCustomResourceController(true)) + .build(); @Test - public void manyResourcesGetCreatedUpdatedAndDeleted() { - integrationTest.teardownIfSuccess( - () -> { - log.info("Creating {} new resources", NUMBER_OF_RESOURCES_CREATED); - for (int i = 0; i < NUMBER_OF_RESOURCES_CREATED; i++) { - TestCustomResource tcr = integrationTest.createTestCustomResource(String.valueOf(i)); - integrationTest - .getCrOperations() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .create(tcr); - } + public void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedException { + log.info("Creating {} new resources", NUMBER_OF_RESOURCES_CREATED); + for (int i = 0; i < NUMBER_OF_RESOURCES_CREATED; i++) { + TestCustomResource tcr = TestUtils.testCustomResourceWithPrefix(String.valueOf(i)); + operator.resources(TestCustomResource.class).create(tcr); + } - await() - .atMost(1, TimeUnit.MINUTES) - .untilAsserted( - () -> { - List items = - integrationTest - .getK8sClient() - .configMaps() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .withLabel( - "managedBy", TestCustomResourceController.class.getSimpleName()) - .list() - .getItems(); - assertThat(items).hasSize(NUMBER_OF_RESOURCES_CREATED); - }); + await() + .atMost(1, TimeUnit.MINUTES) + .untilAsserted( + () -> { + List items = + operator.resources(ConfigMap.class) + .withLabel( + "managedBy", TestCustomResourceController.class.getSimpleName()) + .list() + .getItems(); + assertThat(items).hasSize(NUMBER_OF_RESOURCES_CREATED); + }); - log.info("Updating {} resources", NUMBER_OF_RESOURCES_UPDATED); - // update some resources - for (int i = 0; i < NUMBER_OF_RESOURCES_UPDATED; i++) { - TestCustomResource tcr = - (TestCustomResource) integrationTest - .getCrOperations() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .withName(IntegrationTestSupport.TEST_CUSTOM_RESOURCE_PREFIX + i) - .get(); - tcr.getSpec().setValue(i + UPDATED_SUFFIX); - integrationTest - .getCrOperations() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .createOrReplace(tcr); - } - // sleep for a short time to make variability to the test, so some updates are not - // executed before delete - Thread.sleep(300); + log.info("Updating {} resources", NUMBER_OF_RESOURCES_UPDATED); + // update some resources + for (int i = 0; i < NUMBER_OF_RESOURCES_UPDATED; i++) { + TestCustomResource tcr = + operator.getNamedResource(TestCustomResource.class, + TestUtils.TEST_CUSTOM_RESOURCE_PREFIX + i); + tcr.getSpec().setValue(i + UPDATED_SUFFIX); + operator.resources(TestCustomResource.class) + .createOrReplace(tcr); + } + // sleep for a short time to make variability to the test, so some updates are not + // executed before delete + Thread.sleep(300); - log.info("Deleting {} resources", NUMBER_OF_RESOURCES_DELETED); - for (int i = 0; i < NUMBER_OF_RESOURCES_DELETED; i++) { - TestCustomResource tcr = integrationTest.createTestCustomResource(String.valueOf(i)); - integrationTest - .getCrOperations() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .delete(tcr); - } + log.info("Deleting {} resources", NUMBER_OF_RESOURCES_DELETED); + for (int i = 0; i < NUMBER_OF_RESOURCES_DELETED; i++) { + TestCustomResource tcr = TestUtils.testCustomResourceWithPrefix(String.valueOf(i)); + operator.resources(TestCustomResource.class).delete(tcr); + } - await() - .atMost(1, TimeUnit.MINUTES) - .untilAsserted( - () -> { - List items = - integrationTest - .getK8sClient() - .configMaps() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .withLabel( - "managedBy", TestCustomResourceController.class.getSimpleName()) - .list() - .getItems(); - // reducing configmaps to names only - better for debugging - List itemDescs = - items.stream() - .map(configMap -> configMap.getMetadata().getName()) - .collect(Collectors.toList()); - assertThat(itemDescs) - .hasSize(NUMBER_OF_RESOURCES_CREATED - NUMBER_OF_RESOURCES_DELETED); + await() + .atMost(1, TimeUnit.MINUTES) + .untilAsserted( + () -> { + List items = + operator.resources(ConfigMap.class) + .withLabel( + "managedBy", TestCustomResourceController.class.getSimpleName()) + .list() + .getItems(); + // reducing configmaps to names only - better for debugging + List itemDescs = + items.stream() + .map(configMap -> configMap.getMetadata().getName()) + .collect(Collectors.toList()); + assertThat(itemDescs) + .hasSize(NUMBER_OF_RESOURCES_CREATED - NUMBER_OF_RESOURCES_DELETED); - List crs = - integrationTest - .getK8sClient() - .customResources(TestCustomResource.class) - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .list() - .getItems(); - assertThat(crs) - .hasSize(NUMBER_OF_RESOURCES_CREATED - NUMBER_OF_RESOURCES_DELETED); - }); - }); + List crs = + operator.resources(TestCustomResource.class) + .list() + .getItems(); + assertThat(crs) + .hasSize(NUMBER_OF_RESOURCES_CREATED - NUMBER_OF_RESOURCES_DELETED); + }); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index 843f7a9103..b4bfe376f2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -4,59 +4,44 @@ import static org.awaitility.Awaitility.await; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; +import io.javaoperatorsdk.operator.support.TestUtils; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; -@TestInstance(TestInstance.Lifecycle.PER_METHOD) public class ControllerExecutionIT { - - private IntegrationTestSupport integrationTestSupport = new IntegrationTestSupport(); - - public void initAndCleanup(boolean controllerStatusUpdate) { - KubernetesClient k8sClient = new DefaultKubernetesClient(); - integrationTestSupport.initialize( - k8sClient, new TestCustomResourceController(k8sClient, controllerStatusUpdate)); - integrationTestSupport.cleanup(); - } + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withController(new TestCustomResourceController(true)) + .build(); @Test public void configMapGetsCreatedForTestCustomResource() { - initAndCleanup(true); - integrationTestSupport.teardownIfSuccess( - () -> { - TestCustomResource resource = TestUtils.testCustomResource(); + operator.getControllerOfType(TestCustomResourceController.class).setUpdateStatus(true); - integrationTestSupport - .getCrOperations() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .create(resource); + TestCustomResource resource = TestUtils.testCustomResource(); + operator.create(TestCustomResource.class, resource); - awaitResourcesCreatedOrUpdated(); - awaitStatusUpdated(); - assertThat(integrationTestSupport.numberOfControllerExecutions()).isEqualTo(2); - }); + awaitResourcesCreatedOrUpdated(); + awaitStatusUpdated(); + assertThat(TestUtils.getNumberOfExecutions(operator)).isEqualTo(2); } @Test public void eventIsSkippedChangedOnMetadataOnlyUpdate() { - initAndCleanup(false); - integrationTestSupport.teardownIfSuccess( - () -> { - TestCustomResource resource = TestUtils.testCustomResource(); + operator.getControllerOfType(TestCustomResourceController.class).setUpdateStatus(false); - integrationTestSupport - .getCrOperations() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .create(resource); + TestCustomResource resource = TestUtils.testCustomResource(); + operator.create(TestCustomResource.class, resource); - awaitResourcesCreatedOrUpdated(); - assertThat(integrationTestSupport.numberOfControllerExecutions()).isEqualTo(1); - }); + awaitResourcesCreatedOrUpdated(); + assertThat(TestUtils.getNumberOfExecutions(operator)).isEqualTo(1); } void awaitResourcesCreatedOrUpdated() { @@ -65,12 +50,7 @@ void awaitResourcesCreatedOrUpdated() { .untilAsserted( () -> { ConfigMap configMap = - integrationTestSupport - .getK8sClient() - .configMaps() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .withName("test-config-map") - .get(); + operator.getNamedResource(ConfigMap.class, "test-config-map"); assertThat(configMap).isNotNull(); assertThat(configMap.getData().get("test-key")).isEqualTo("test-value"); }); @@ -86,11 +66,8 @@ void awaitStatusUpdated(int timeout) { .untilAsserted( () -> { TestCustomResource cr = - (TestCustomResource) integrationTestSupport - .getCrOperations() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .withName(TestUtils.TEST_CUSTOM_RESOURCE_NAME) - .get(); + operator.getNamedResource(TestCustomResource.class, + TestUtils.TEST_CUSTOM_RESOURCE_NAME); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); assertThat(cr.getStatus().getConfigMapStatus()).isEqualTo("ConfigMap Ready"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index 05a4a3df8f..771c5c763b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -1,52 +1,42 @@ package io.javaoperatorsdk.operator; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResource; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResourceController; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResourceSpec; -import org.junit.jupiter.api.BeforeEach; +import io.javaoperatorsdk.operator.support.TestUtils; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.junit.jupiter.api.extension.RegisterExtension; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class EventSourceIT { - - private static final Logger log = LoggerFactory.getLogger(EventSourceIT.class); - - public static final int EXPECTED_TIMER_EVENT_COUNT = 3; - private final IntegrationTestSupport integrationTestSupport = new IntegrationTestSupport(); - - @BeforeEach - public void initAndCleanup() { - KubernetesClient k8sClient = new DefaultKubernetesClient(); - integrationTestSupport.initialize(k8sClient, new EventSourceTestCustomResourceController()); - integrationTestSupport.cleanup(); - } + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withController(EventSourceTestCustomResourceController.class) + .build(); @Test public void receivingPeriodicEvents() { - integrationTestSupport.teardownIfSuccess( - () -> { - EventSourceTestCustomResource resource = createTestCustomResource("1"); - integrationTestSupport - .getCrOperations() - .inNamespace(IntegrationTestSupport.TEST_NAMESPACE) - .create(resource); - - Thread.sleep( - EventSourceTestCustomResourceController.TIMER_DELAY - + EXPECTED_TIMER_EVENT_COUNT - * EventSourceTestCustomResourceController.TIMER_PERIOD); - - assertThat(integrationTestSupport.numberOfControllerExecutions()) - .isGreaterThanOrEqualTo(EXPECTED_TIMER_EVENT_COUNT + 1); - }); + EventSourceTestCustomResource resource = createTestCustomResource("1"); + + operator.create(EventSourceTestCustomResource.class, resource); + + await() + .atMost(5, TimeUnit.SECONDS) + .pollInterval( + EventSourceTestCustomResourceController.TIMER_PERIOD / 2, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> { + assertThat(TestUtils.getNumberOfExecutions(operator)) + .isGreaterThanOrEqualTo(4); + }); } public EventSourceTestCustomResource createTestCustomResource(String id) { @@ -54,7 +44,7 @@ public EventSourceTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("eventsource-" + id) - .withNamespace(IntegrationTestSupport.TEST_NAMESPACE) + .withNamespace(operator.getNamespace()) .withFinalizers(EventSourceTestCustomResourceController.FINALIZER_NAME) .build()); resource.setKind("Eventsourcesample"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java deleted file mode 100644 index 271c50514b..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/IntegrationTestSupport.java +++ /dev/null @@ -1,198 +0,0 @@ -package io.javaoperatorsdk.operator; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.api.model.Namespace; -import io.fabric8.kubernetes.api.model.NamespaceBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; -import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; -import io.fabric8.kubernetes.client.utils.Serialization; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; -import io.javaoperatorsdk.operator.processing.retry.Retry; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceSpec; - -public class IntegrationTestSupport { - - public static final String TEST_NAMESPACE = "java-operator-sdk-int-test"; - public static final String TEST_CUSTOM_RESOURCE_PREFIX = "test-custom-resource-"; - private static final Logger log = LoggerFactory.getLogger(IntegrationTestSupport.class); - private KubernetesClient k8sClient; - private MixedOperation, Resource> crOperations; - private Operator operator; - private ResourceController controller; - - public void initialize(KubernetesClient k8sClient, ResourceController controller) { - initialize(k8sClient, controller, null); - } - - public void initialize(KubernetesClient k8sClient, ResourceController controller, Retry retry) { - log.info("Initializing integration test in namespace {}", TEST_NAMESPACE); - this.k8sClient = k8sClient; - this.controller = controller; - - final var configurationService = DefaultConfigurationService.instance(); - - final var config = configurationService.getConfigurationFor(controller); - // load generated CRD - final var crdPath = "/META-INF/fabric8/" + config.getCRDName() + "-v1.yml"; - loadCRDAndApplyToCluster(crdPath); - - final var customResourceClass = config.getCustomResourceClass(); - this.crOperations = k8sClient.resources(customResourceClass); - - final var namespaces = k8sClient.namespaces(); - if (namespaces.withName(TEST_NAMESPACE).get() == null) { - namespaces.create( - new NamespaceBuilder().withNewMetadata().withName(TEST_NAMESPACE).endMetadata().build()); - } - operator = new Operator(k8sClient, configurationService); - final var overriddenConfig = - ControllerConfigurationOverrider.override(config).settingNamespace(TEST_NAMESPACE); - if (retry != null) { - overriddenConfig.withRetry(retry); - } - operator.register(controller, overriddenConfig.build()); - operator.start(); - log.info("Operator is running with {}", controller.getClass().getCanonicalName()); - } - - public void loadCRDAndApplyToCluster(String classPathYaml) { - var crd = loadYaml(CustomResourceDefinition.class, classPathYaml); - if ("apiextensions.k8s.io/v1".equals(crd.getApiVersion())) { - k8sClient.apiextensions().v1().customResourceDefinitions().createOrReplace(crd); - } else { - var crd2 = - loadYaml( - io.fabric8.kubernetes.api.model.apiextensions.v1beta1.CustomResourceDefinition.class, - classPathYaml); - k8sClient.apiextensions().v1beta1().customResourceDefinitions().createOrReplace(crd2); - } - } - - public void cleanup() { - log.info("Cleaning up namespace {}", TEST_NAMESPACE); - - // we depend on the actual operator from the startup to handle the finalizers and clean up - // resources from previous test runs - crOperations.inNamespace(TEST_NAMESPACE).delete(crOperations.list().getItems()); - - await("all CRs cleaned up") - .atMost(60, TimeUnit.SECONDS) - .untilAsserted( - () -> assertThat(crOperations.inNamespace(TEST_NAMESPACE).list().getItems()).isEmpty()); - - k8sClient - .configMaps() - .inNamespace(TEST_NAMESPACE) - .withLabel("managedBy", controller.getClass().getSimpleName()) - .delete(); - - await("all config maps cleaned up") - .atMost(60, TimeUnit.SECONDS) - .untilAsserted( - () -> { - assertThat( - k8sClient - .configMaps() - .inNamespace(TEST_NAMESPACE) - .withLabel("managedBy", controller.getClass().getSimpleName()) - .list() - .getItems() - .isEmpty()); - }); - - log.info("Cleaned up namespace " + TEST_NAMESPACE); - } - - /** - * Use this method to execute the cleanup of the integration test namespace only in case the test - * was successful. This is useful to keep the Kubernetes resources around to debug a failed test - * run. Unfortunately I couldn't make this work with standard JUnit methods as the @AfterAll - * method doesn't know if the tests succeeded or not. - * - * @param test The code of the actual test. - * @throws Exception if the test threw an exception. - */ - public void teardownIfSuccess(TestRun test) { - try { - test.run(); - - log.info("Deleting namespace {} and stopping operator", TEST_NAMESPACE); - Namespace namespace = k8sClient.namespaces().withName(TEST_NAMESPACE).get(); - if (namespace.getStatus().getPhase().equals("Active")) { - k8sClient.namespaces().withName(TEST_NAMESPACE).delete(); - } - await("namespace deleted") - .atMost(45, TimeUnit.SECONDS) - .until(() -> k8sClient.namespaces().withName(TEST_NAMESPACE).get() == null); - } catch (Exception e) { - throw new IllegalStateException(e); - } finally { - k8sClient.close(); - } - } - - public int numberOfControllerExecutions() { - return ((TestExecutionInfoProvider) controller).getNumberOfExecutions(); - } - - private T loadYaml(Class clazz, String yaml) { - try (InputStream is = getClass().getResourceAsStream(yaml)) { - return Serialization.unmarshal(is, clazz); - } catch (IOException ex) { - throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); - } - } - - public TestCustomResource createTestCustomResource(String id) { - TestCustomResource resource = new TestCustomResource(); - resource.setMetadata( - new ObjectMetaBuilder() - .withName(TEST_CUSTOM_RESOURCE_PREFIX + id) - .withNamespace(TEST_NAMESPACE) - .build()); - resource.setKind("CustomService"); - resource.setSpec(new TestCustomResourceSpec()); - resource.getSpec().setConfigMapName("test-config-map-" + id); - resource.getSpec().setKey("test-key"); - resource.getSpec().setValue(id); - return resource; - } - - public KubernetesClient getK8sClient() { - return k8sClient; - } - - public MixedOperation, Resource> getCrOperations() { - return crOperations; - } - - public CustomResource getCustomResource(String name) { - return getCrOperations().inNamespace(TEST_NAMESPACE).withName(name).get(); - } - - public Operator getOperator() { - return operator; - } - - public interface TestRun { - - void run() throws Exception; - } -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index bdbb5f561c..b3b4be5f95 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -1,54 +1,57 @@ package io.javaoperatorsdk.operator; -import static io.javaoperatorsdk.operator.IntegrationTestSupport.TEST_NAMESPACE; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; -import io.javaoperatorsdk.operator.processing.retry.Retry; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResource; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResourceController; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResourceStatus; -import org.junit.jupiter.api.BeforeEach; +import io.javaoperatorsdk.operator.support.TestUtils; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; public class RetryIT { - public static final int RETRY_INTERVAL = 150; - private IntegrationTestSupport integrationTestSupport = new IntegrationTestSupport(); - @BeforeEach - public void initAndCleanup() { - Retry retry = - new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry().setMaxAttempts(5); - KubernetesClient k8sClient = new DefaultKubernetesClient(); - integrationTestSupport.initialize(k8sClient, new RetryTestCustomResourceController(), retry); - integrationTestSupport.cleanup(); - } + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withController( + new RetryTestCustomResourceController(), + new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry() + .setMaxAttempts(5)) + .build(); + @Test public void retryFailedExecution() { - integrationTestSupport.teardownIfSuccess( - () -> { - RetryTestCustomResource resource = createTestCustomResource("1"); - integrationTestSupport.getCrOperations().inNamespace(TEST_NAMESPACE).create(resource); + RetryTestCustomResource resource = createTestCustomResource("1"); - Thread.sleep( - RETRY_INTERVAL * (RetryTestCustomResourceController.NUMBER_FAILED_EXECUTIONS + 2)); + operator.create(RetryTestCustomResource.class, resource); - assertThat(integrationTestSupport.numberOfControllerExecutions()) - .isGreaterThanOrEqualTo( - RetryTestCustomResourceController.NUMBER_FAILED_EXECUTIONS + 1); + await("cr status updated") + .pollDelay( + RETRY_INTERVAL * (RetryTestCustomResourceController.NUMBER_FAILED_EXECUTIONS + 2), + TimeUnit.MILLISECONDS) + .pollInterval( + RETRY_INTERVAL, + TimeUnit.MILLISECONDS) + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertThat( + TestUtils.getNumberOfExecutions(operator)) + .isEqualTo(RetryTestCustomResourceController.NUMBER_FAILED_EXECUTIONS + 1); RetryTestCustomResource finalResource = - (RetryTestCustomResource) integrationTestSupport - .getCrOperations() - .inNamespace(TEST_NAMESPACE) - .withName(resource.getMetadata().getName()) - .get(); + operator.getNamedResource(RetryTestCustomResource.class, + resource.getMetadata().getName()); assertThat(finalResource.getStatus().getState()) .isEqualTo(RetryTestCustomResourceStatus.State.SUCCESS); }); @@ -59,7 +62,6 @@ public RetryTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("retrysource-" + id) - .withNamespace(TEST_NAMESPACE) .withFinalizers(RetryTestCustomResourceController.FINALIZER_NAME) .build()); resource.setKind("retrysample"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 0a7cbada4d..ac7fa95c86 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -1,81 +1,73 @@ package io.javaoperatorsdk.operator; -import static io.javaoperatorsdk.operator.IntegrationTestSupport.TEST_NAMESPACE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; +import io.javaoperatorsdk.operator.support.TestUtils; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResource; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceController; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceStatus; -import java.util.Collections; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class SubResourceUpdateIT { +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; - private IntegrationTestSupport integrationTestSupport = new IntegrationTestSupport(); +public class SubResourceUpdateIT { - @BeforeEach - public void initAndCleanup() { - KubernetesClient k8sClient = new DefaultKubernetesClient(); - integrationTestSupport.initialize(k8sClient, new SubResourceTestCustomResourceController()); - integrationTestSupport.cleanup(); - } + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withController(SubResourceTestCustomResourceController.class) + .build(); @Test public void updatesSubResourceStatus() { - integrationTestSupport.teardownIfSuccess( - () -> { - SubResourceTestCustomResource resource = createTestCustomResource("1"); - integrationTestSupport.getCrOperations().inNamespace(TEST_NAMESPACE).create(resource); - - awaitStatusUpdated(resource.getMetadata().getName()); - // wait for sure, there are no more events - waitXms(200); - // there is no event on status update processed - assertThat(integrationTestSupport.numberOfControllerExecutions()).isEqualTo(2); - }); + SubResourceTestCustomResource resource = createTestCustomResource("1"); + operator.create(SubResourceTestCustomResource.class, resource); + + awaitStatusUpdated(resource.getMetadata().getName()); + // wait for sure, there are no more events + waitXms(200); + // there is no event on status update processed + assertThat(TestUtils.getNumberOfExecutions(operator)) + .isEqualTo(2); } @Test public void updatesSubResourceStatusNoFinalizer() { - integrationTestSupport.teardownIfSuccess( - () -> { - SubResourceTestCustomResource resource = createTestCustomResource("1"); - resource.getMetadata().setFinalizers(Collections.EMPTY_LIST); - - integrationTestSupport.getCrOperations().inNamespace(TEST_NAMESPACE).create(resource); - - awaitStatusUpdated(resource.getMetadata().getName()); - // wait for sure, there are no more events - waitXms(200); - // there is no event on status update processed - assertThat(integrationTestSupport.numberOfControllerExecutions()).isEqualTo(2); - }); + SubResourceTestCustomResource resource = createTestCustomResource("1"); + resource.getMetadata().setFinalizers(Collections.emptyList()); + + operator.create(SubResourceTestCustomResource.class, resource); + + awaitStatusUpdated(resource.getMetadata().getName()); + // wait for sure, there are no more events + waitXms(200); + // there is no event on status update processed + assertThat(TestUtils.getNumberOfExecutions(operator)) + .isEqualTo(2); } /** Note that we check on controller impl if there is finalizer on execution. */ @Test public void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { - integrationTestSupport.teardownIfSuccess( - () -> { - SubResourceTestCustomResource resource = createTestCustomResource("1"); - resource.getMetadata().getFinalizers().clear(); - integrationTestSupport.getCrOperations().inNamespace(TEST_NAMESPACE).create(resource); - - awaitStatusUpdated(resource.getMetadata().getName()); - // wait for sure, there are no more events - waitXms(200); - // there is no event on status update processed - assertThat(integrationTestSupport.numberOfControllerExecutions()).isEqualTo(2); - }); + SubResourceTestCustomResource resource = createTestCustomResource("1"); + resource.getMetadata().getFinalizers().clear(); + operator.create(SubResourceTestCustomResource.class, resource); + + awaitStatusUpdated(resource.getMetadata().getName()); + // wait for sure, there are no more events + waitXms(200); + // there is no event on status update processed + assertThat(TestUtils.getNumberOfExecutions(operator)) + .isEqualTo(2); } /** @@ -86,24 +78,19 @@ public void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain */ @Test public void updateCustomResourceAfterSubResourceChange() { - integrationTestSupport.teardownIfSuccess( - () -> { - SubResourceTestCustomResource resource = createTestCustomResource("1"); - integrationTestSupport.getCrOperations().inNamespace(TEST_NAMESPACE).create(resource); - - resource.getSpec().setValue("new value"); - integrationTestSupport - .getCrOperations() - .inNamespace(TEST_NAMESPACE) - .createOrReplace(resource); - - awaitStatusUpdated(resource.getMetadata().getName()); - - // wait for sure, there are no more events - waitXms(200); - // there is no event on status update processed - assertThat(integrationTestSupport.numberOfControllerExecutions()).isEqualTo(3); - }); + SubResourceTestCustomResource resource = createTestCustomResource("1"); + operator.create(SubResourceTestCustomResource.class, resource); + + resource.getSpec().setValue("new value"); + operator.resources(SubResourceTestCustomResource.class).createOrReplace(resource); + + awaitStatusUpdated(resource.getMetadata().getName()); + + // wait for sure, there are no more events + waitXms(200); + // there is no event on status update processed + assertThat(TestUtils.getNumberOfExecutions(operator)) + .isEqualTo(3); } void awaitStatusUpdated(String name) { @@ -112,11 +99,7 @@ void awaitStatusUpdated(String name) { .untilAsserted( () -> { SubResourceTestCustomResource cr = - (SubResourceTestCustomResource) integrationTestSupport - .getCrOperations() - .inNamespace(TEST_NAMESPACE) - .withName(name) - .get(); + operator.getNamedResource(SubResourceTestCustomResource.class, name); assertThat(cr.getMetadata().getFinalizers()).hasSize(1); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); @@ -130,7 +113,6 @@ public SubResourceTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("subresource-" + id) - .withNamespace(TEST_NAMESPACE) .withFinalizers(SubResourceTestCustomResourceController.FINALIZER_NAME) .build()); resource.setKind("SubresourceSample"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index f29fb7c4f9..9d4e0fd30c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -1,57 +1,54 @@ package io.javaoperatorsdk.operator; -import static io.javaoperatorsdk.operator.IntegrationTestSupport.TEST_NAMESPACE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; +import io.javaoperatorsdk.operator.support.TestUtils; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResource; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceController; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceStatus; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class UpdatingResAndSubResIT { - - private IntegrationTestSupport integrationTestSupport = new IntegrationTestSupport(); +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; - @BeforeEach - public void initAndCleanup() { - KubernetesClient k8sClient = new DefaultKubernetesClient(); - integrationTestSupport.initialize(k8sClient, new DoubleUpdateTestCustomResourceController()); - integrationTestSupport.cleanup(); - } +public class UpdatingResAndSubResIT { + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withController(DoubleUpdateTestCustomResourceController.class) + .build(); @Test public void updatesSubResourceStatus() { - integrationTestSupport.teardownIfSuccess( - () -> { - DoubleUpdateTestCustomResource resource = createTestCustomResource("1"); - integrationTestSupport.getCrOperations().inNamespace(TEST_NAMESPACE).create(resource); + DoubleUpdateTestCustomResource resource = createTestCustomResource("1"); + operator.create(DoubleUpdateTestCustomResource.class, resource); + + awaitStatusUpdated(resource.getMetadata().getName()); + // wait for sure, there are no more events + TestUtils.waitXms(300); - awaitStatusUpdated(resource.getMetadata().getName()); - // wait for sure, there are no more events - TestUtils.waitXms(300); + DoubleUpdateTestCustomResource customResource = + operator + .getNamedResource(DoubleUpdateTestCustomResource.class, + resource.getMetadata().getName()); - DoubleUpdateTestCustomResource customResource = - (DoubleUpdateTestCustomResource) integrationTestSupport - .getCustomResource(resource.getMetadata().getName()); - assertThat(integrationTestSupport.numberOfControllerExecutions()).isEqualTo(1); - assertThat(customResource.getStatus().getState()) - .isEqualTo(DoubleUpdateTestCustomResourceStatus.State.SUCCESS); - assertThat( - customResource - .getMetadata() - .getAnnotations() - .get(DoubleUpdateTestCustomResourceController.TEST_ANNOTATION)) - .isNotNull(); - }); + assertThat(TestUtils.getNumberOfExecutions(operator)) + .isEqualTo(1); + assertThat(customResource.getStatus().getState()) + .isEqualTo(DoubleUpdateTestCustomResourceStatus.State.SUCCESS); + assertThat( + customResource + .getMetadata() + .getAnnotations() + .get(DoubleUpdateTestCustomResourceController.TEST_ANNOTATION)) + .isNotNull(); } void awaitStatusUpdated(String name) { @@ -60,14 +57,14 @@ void awaitStatusUpdated(String name) { .untilAsserted( () -> { DoubleUpdateTestCustomResource cr = - (DoubleUpdateTestCustomResource) integrationTestSupport - .getCrOperations() - .inNamespace(TEST_NAMESPACE) - .withName(name) - .get(); - assertThat(cr.getMetadata().getFinalizers()).hasSize(1); - assertThat(cr).isNotNull(); - assertThat(cr.getStatus()).isNotNull(); + operator.getNamedResource(DoubleUpdateTestCustomResource.class, name); + + assertThat(cr) + .isNotNull(); + assertThat(cr.getMetadata().getFinalizers()) + .hasSize(1); + assertThat(cr.getStatus()) + .isNotNull(); assertThat(cr.getStatus().getState()) .isEqualTo(DoubleUpdateTestCustomResourceStatus.State.SUCCESS); }); @@ -75,11 +72,7 @@ void awaitStatusUpdated(String name) { public DoubleUpdateTestCustomResource createTestCustomResource(String id) { DoubleUpdateTestCustomResource resource = new DoubleUpdateTestCustomResource(); - resource.setMetadata( - new ObjectMetaBuilder() - .withName("doubleupdateresource-" + id) - .withNamespace(TEST_NAMESPACE) - .build()); + resource.setMetadata(new ObjectMetaBuilder().withName("doubleupdateresource-" + id).build()); resource.setKind("DoubleUpdateSample"); resource.setSpec(new DoubleUpdateTestCustomResourceSpec()); resource.getSpec().setValue(id); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index b0205bd4ef..0252ae8ac1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -28,6 +28,8 @@ public class DefaultConfigurationServiceTest { @Test void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { + final var configurationService = DefaultConfigurationService.instance(); + final LoggerContext context = LoggerContext.getContext(false); final PatternLayout layout = PatternLayout.createDefaultLayout(context.getConfiguration()); final ListAppender appender = new ListAppender("list", null, layout, false, false); @@ -37,11 +39,12 @@ void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { context.getConfiguration().addAppender(appender); AppenderRef ref = AppenderRef.createAppenderRef("list", null, null); + final var loggerName = configurationService.getLoggerName(); LoggerConfig loggerConfig = LoggerConfig.createLogger( false, Level.valueOf("info"), - AbstractConfigurationService.LOGGER_NAME, + loggerName, "false", new AppenderRef[] {ref}, null, @@ -49,12 +52,12 @@ void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { null); loggerConfig.addAppender(appender, null, null); - context.getConfiguration().addLogger(AbstractConfigurationService.LOGGER_NAME, loggerConfig); + context.getConfiguration().addLogger(loggerName, loggerConfig); context.updateLoggers(); try { final var config = - DefaultConfigurationService.instance() + configurationService .getConfigurationFor(new NotAutomaticallyCreated(), false); assertThat(config).isNull(); @@ -64,7 +67,7 @@ void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { } finally { appender.stop(); - context.getConfiguration().removeLogger(AbstractConfigurationService.LOGGER_NAME); + context.getConfiguration().removeLogger(loggerName); context.updateLoggers(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java index e3959681d9..37dcec276c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator.sample.doubleupdate; -import io.javaoperatorsdk.operator.TestExecutionInfoProvider; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.*; import java.util.HashMap; import java.util.concurrent.atomic.AtomicInteger; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java index 91e4e33298..be4c404fda 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java @@ -2,7 +2,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.TestExecutionInfoProvider; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java index 512a0a55a4..847f247872 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java @@ -2,7 +2,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.TestExecutionInfoProvider; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java index 0ca5413f31..e26b629a74 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java @@ -6,12 +6,13 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.TestExecutionInfoProvider; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.DeleteControl; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -20,26 +21,44 @@ @Controller(generationAwareEventProcessing = false) public class TestCustomResourceController - implements ResourceController, TestExecutionInfoProvider { + implements ResourceController, TestExecutionInfoProvider, + KubernetesClientAware { private static final Logger log = LoggerFactory.getLogger(TestCustomResourceController.class); public static final String FINALIZER_NAME = ControllerUtils.getDefaultFinalizerName(CustomResource.getCRDName(TestCustomResource.class)); - private final KubernetesClient kubernetesClient; - private final boolean updateStatus; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private KubernetesClient kubernetesClient; + private boolean updateStatus; - public TestCustomResourceController(KubernetesClient kubernetesClient) { - this(kubernetesClient, true); + public TestCustomResourceController() { + this(true); } - public TestCustomResourceController(KubernetesClient kubernetesClient, boolean updateStatus) { - this.kubernetesClient = kubernetesClient; + public TestCustomResourceController(boolean updateStatus) { this.updateStatus = updateStatus; } + public boolean isUpdateStatus() { + return updateStatus; + } + + public void setUpdateStatus(boolean updateStatus) { + this.updateStatus = updateStatus; + } + + @Override + public KubernetesClient getKubernetesClient() { + return kubernetesClient; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + } + @Override public DeleteControl deleteResource( TestCustomResource resource, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java index 50f0285b22..8084ad7cfc 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java @@ -2,7 +2,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.TestExecutionInfoProvider; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/TestExecutionInfoProvider.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestExecutionInfoProvider.java similarity index 64% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/TestExecutionInfoProvider.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestExecutionInfoProvider.java index caf0211df4..f2f634041d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/TestExecutionInfoProvider.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestExecutionInfoProvider.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator; +package io.javaoperatorsdk.operator.support; public interface TestExecutionInfoProvider { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java similarity index 60% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/TestUtils.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java index 2029970f0f..76ae1c4991 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java @@ -1,6 +1,7 @@ -package io.javaoperatorsdk.operator; +package io.javaoperatorsdk.operator.support; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceSpec; import java.util.HashMap; @@ -8,6 +9,7 @@ public class TestUtils { + public static final String TEST_CUSTOM_RESOURCE_PREFIX = "test-custom-resource-"; public static final String TEST_CUSTOM_RESOURCE_NAME = "test-custom-resource"; public static final String TEST_NAMESPACE = "java-operator-sdk-int-test"; @@ -22,7 +24,6 @@ public static TestCustomResource testCustomResource(String uid) { .withName(TEST_CUSTOM_RESOURCE_NAME) .withUid(uid) .withGeneration(1L) - .withNamespace(TEST_NAMESPACE) .build()); resource.getMetadata().setAnnotations(new HashMap<>()); resource.setKind("CustomService"); @@ -33,6 +34,20 @@ public static TestCustomResource testCustomResource(String uid) { return resource; } + public static TestCustomResource testCustomResourceWithPrefix(String id) { + TestCustomResource resource = new TestCustomResource(); + resource.setMetadata( + new ObjectMetaBuilder() + .withName(TEST_CUSTOM_RESOURCE_PREFIX + id) + .build()); + resource.setKind("CustomService"); + resource.setSpec(new TestCustomResourceSpec()); + resource.getSpec().setConfigMapName("test-config-map-" + id); + resource.getSpec().setKey("test-key"); + resource.getSpec().setValue(id); + return resource; + } + public static void waitXms(int x) { try { Thread.sleep(x); @@ -40,4 +55,8 @@ public static void waitXms(int x) { throw new IllegalStateException(e); } } + + public static int getNumberOfExecutions(OperatorExtension extension) { + return ((TestExecutionInfoProvider) extension.getControllers().get(0)).getNumberOfExecutions(); + } } diff --git a/pom.xml b/pom.xml index 18914e47c7..901dd73ee4 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ operator-framework-core + operator-framework-junit5 operator-framework samples @@ -202,6 +203,9 @@ org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} + + 3 + org.apache.maven.plugins From b558a6bf2164f0ba7d13760630d85cd977b50ba1 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Mon, 20 Sep 2021 14:48:35 +0200 Subject: [PATCH 0052/1608] feat: add impsort-maven-plugin #539 --- .github/workflows/pr.yml | 4 +- .gitignore | 1 + contributing/eclipse.importorder | 7 ++++ .../operator/ControllerUtils.java | 3 +- .../operator/CustomResourceUtils.java | 3 +- .../operator/EventListUtils.java | 3 +- .../io/javaoperatorsdk/operator/Operator.java | 1 - .../javaoperatorsdk/operator/api/Context.java | 3 +- .../operator/api/DefaultContext.java | 3 +- .../config/AbstractConfigurationService.java | 7 ++-- .../AbstractControllerConfiguration.java | 3 +- .../api/config/ConfigurationService.java | 6 ++- .../config/ConfigurationServiceOverrider.java | 6 +-- .../api/config/ControllerConfiguration.java | 7 ++-- .../processing/CustomResourceCache.java | 15 ++++--- .../processing/DefaultEventHandler.java | 8 ++-- .../operator/processing/EventBuffer.java | 3 +- .../operator/processing/EventDispatcher.java | 10 ++--- .../processing/event/AbstractEvent.java | 3 +- .../processing/event/DefaultEvent.java | 3 +- .../operator/processing/event/Event.java | 3 +- .../event/internal/CustomResourceEvent.java | 4 +- .../internal/CustomResourceEventSource.java | 8 ++-- .../event/internal/TimerEventSource.java | 8 ++-- .../processing/retry/RetryExecution.java | 3 +- .../operator/ControllerUtilsTest.java | 5 ++- .../operator/CustomResourceUtilsTest.java | 5 ++- .../javaoperatorsdk/operator/TestUtils.java | 5 ++- .../config/ControllerConfigurationTest.java | 5 ++- .../CustomResourceSelectorTest.java | 20 ++++----- .../processing/DefaultEventHandlerTest.java | 26 ++++++------ .../operator/processing/EventBufferTest.java | 10 +++-- .../processing/EventDispatcherTest.java | 26 ++++++------ .../event/DefaultEventSourceManagerTest.java | 16 +++---- .../processing/event/EventListTest.java | 10 +++-- .../CustomResourceEventSourceTest.java | 14 +++---- .../internal/CustomResourceSelectorTest.java | 42 ++++++++++--------- .../event/internal/TimerEventSourceTest.java | 20 +++++---- .../retry/GenericRetryExecutionTest.java | 7 ++-- .../simple/TestCustomResourceController.java | 10 +++-- .../operator/junit/OperatorExtension.java | 32 +++++++------- .../runtime/AccumulativeMappingWriter.java | 1 + .../runtime/AnnotationConfiguration.java | 7 ++-- .../config/runtime/ClassMappingProvider.java | 1 + .../ControllerAnnotationProcessor.java | 15 ++++--- .../runtime/RuntimeControllerMetadata.java | 3 +- .../config/runtime/TypeParameterResolver.java | 7 ++-- .../operator/ConcurrencyIT.java | 20 +++++---- .../operator/ControllerExecutionIT.java | 12 +++--- .../operator/EventSourceIT.java | 12 +++--- .../io/javaoperatorsdk/operator/RetryIT.java | 12 +++--- .../operator/SubResourceUpdateIT.java | 2 +- .../operator/UpdatingResAndSubResIT.java | 2 +- .../ControllerAnnotationProcessorTest.java | 3 +- .../DefaultConfigurationServiceTest.java | 24 +++++------ ...bleUpdateTestCustomResourceController.java | 6 ++- ...entSourceTestCustomResourceController.java | 10 +++-- .../RetryTestCustomResourceController.java | 10 +++-- .../simple/TestCustomResourceController.java | 14 ++++--- ...bResourceTestCustomResourceController.java | 10 +++-- .../operator/support/TestUtils.java | 5 ++- pom.xml | 34 ++++++++++++++- .../sample/CustomServiceController.java | 8 ++-- .../operator/sample/Config.java | 8 ++-- 64 files changed, 357 insertions(+), 247 deletions(-) create mode 100644 contributing/eclipse.importorder diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a5c4c0609d..44fffbdeba 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -27,7 +27,9 @@ jobs: java-version: ${{ matrix.java }} cache: 'maven' - name: Check code format - run: ./mvnw ${MAVEN_ARGS} formatter:validate -Dconfigfile=$PWD/contributing/eclipse-google-style.xml --file pom.xml + run: | + ./mvnw ${MAVEN_ARGS} formatter:validate -Dconfigfile=$PWD/contributing/eclipse-google-style.xml --file pom.xml + ./mvnw ${MAVEN_ARGS} impsort:check --file pom.xml - name: Run unit tests run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml - name: Set up Minikube diff --git a/.gitignore b/.gitignore index 36ed54bccc..d82a982579 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ target/ .settings/ .classpath .project +.cache/ # VSCode .factorypath diff --git a/contributing/eclipse.importorder b/contributing/eclipse.importorder new file mode 100644 index 0000000000..8f15ca7dd3 --- /dev/null +++ b/contributing/eclipse.importorder @@ -0,0 +1,7 @@ +0=java +1=javax +2=org +2=io +4=com +5= +6=\# diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java index 55cbe9d6f6..0097cb128a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator; +import java.util.Locale; + import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; -import java.util.Locale; public class ControllerUtils { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/CustomResourceUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/CustomResourceUtils.java index 868437af57..d49c373e76 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/CustomResourceUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/CustomResourceUtils.java @@ -1,9 +1,10 @@ package io.javaoperatorsdk.operator; +import java.util.Arrays; + import io.fabric8.kubernetes.api.model.Cluster; import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; -import java.util.Arrays; public abstract class CustomResourceUtils { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java index 19a976c38e..852b630af0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java @@ -1,9 +1,10 @@ package io.javaoperatorsdk.operator; +import java.util.List; + import io.fabric8.kubernetes.client.Watcher; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; -import java.util.List; public class EventListUtils { 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 b2dfef58dd..63df0a79c3 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 @@ -6,7 +6,6 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java index 10cba18985..963eb16f76 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.api; +import java.util.Optional; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.event.EventList; -import java.util.Optional; public interface Context { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java index d650b8aa3b..4614b562df 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.api; +import java.util.Optional; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.event.EventList; -import java.util.Optional; public class DefaultContext implements Context { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index e85a4e8c4b..9983e2d6a6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -1,13 +1,14 @@ package io.javaoperatorsdk.operator.api.config; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.ResourceController; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.api.ResourceController; + public abstract class AbstractConfigurationService implements ConfigurationService { private final Map configurations = new ConcurrentHashMap<>(); private final Version version; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java index 2051a4945f..e3780fc73e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.api.config; -import io.fabric8.kubernetes.client.CustomResource; import java.util.Set; +import io.fabric8.kubernetes.client.CustomResource; + /** * @deprecated use {@link DefaultControllerConfiguration} instead * @param 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 b834b32f21..d6075e75e5 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 @@ -1,11 +1,13 @@ package io.javaoperatorsdk.operator.api.config; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Set; + import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.ResourceController; -import java.util.Set; + +import com.fasterxml.jackson.databind.ObjectMapper; /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 64a2d5c050..ed0bd513c6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -1,13 +1,13 @@ package io.javaoperatorsdk.operator.api.config; -import io.javaoperatorsdk.operator.api.ResourceController; import java.util.Set; -import com.fasterxml.jackson.databind.ObjectMapper; - import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.Metrics; +import io.javaoperatorsdk.operator.api.ResourceController; + +import com.fasterxml.jackson.databind.ObjectMapper; public class ConfigurationServiceOverrider { private final ConfigurationService original; 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 89d4872303..bcc068cec6 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 @@ -1,12 +1,13 @@ package io.javaoperatorsdk.operator.api.config; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Controller; import java.lang.reflect.ParameterizedType; import java.util.Collections; import java.util.Set; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.api.Controller; + public interface ControllerConfiguration { default String getName() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java index 95f85d26d0..74c832a323 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java @@ -1,11 +1,5 @@ package io.javaoperatorsdk.operator.processing; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.fabric8.kubernetes.client.CustomResource; import java.util.List; import java.util.Optional; import java.util.Set; @@ -15,9 +9,18 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.client.CustomResource; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; + @SuppressWarnings("rawtypes") public class CustomResourceCache { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 4c33ec23b1..d2ef3d3416 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -1,9 +1,5 @@ package io.javaoperatorsdk.operator.processing; -import static io.javaoperatorsdk.operator.EventListUtils.containsCustomResourceDeletedEvent; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; - import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -27,6 +23,10 @@ import io.javaoperatorsdk.operator.processing.retry.Retry; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; +import static io.javaoperatorsdk.operator.EventListUtils.containsCustomResourceDeletedEvent; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; + /** * Event handler that makes sure that events are processed in a "single threaded" way per resource * UID, while buffering events which are received during an execution. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java index 692eb7d44b..c9d248acf2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.processing; -import io.javaoperatorsdk.operator.processing.event.Event; import java.util.*; +import io.javaoperatorsdk.operator.processing.event.Event; + class EventBuffer { private final Map> events = new HashMap<>(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 8eef425dad..7ed0e25aca 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -1,10 +1,5 @@ package io.javaoperatorsdk.operator.processing; -import static io.javaoperatorsdk.operator.EventListUtils.containsCustomResourceDeletedEvent; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,6 +16,11 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.EventList; +import static io.javaoperatorsdk.operator.EventListUtils.containsCustomResourceDeletedEvent; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; + /** * Dispatches events to the Controller and handles Finalizers for a single type of Custom Resource. */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java index d6d7273b7f..3429301fe1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.processing.event; -import io.fabric8.kubernetes.client.CustomResource; import java.util.function.Predicate; +import io.fabric8.kubernetes.client.CustomResource; + /** * @deprecated use {@link DefaultEvent} instead */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java index a1c2728a8b..fd48d1859c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java @@ -1,9 +1,10 @@ package io.javaoperatorsdk.operator.processing.event; -import io.fabric8.kubernetes.client.CustomResource; import java.util.Objects; import java.util.function.Predicate; +import io.fabric8.kubernetes.client.CustomResource; + @SuppressWarnings("rawtypes") public class DefaultEvent implements Event { private final Predicate customResourcesSelector; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java index d10d0a8e9a..095467fd39 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.processing.event; -import io.fabric8.kubernetes.client.CustomResource; import java.util.function.Predicate; +import io.fabric8.kubernetes.client.CustomResource; + public interface Event { /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java index c0cc8b1a08..007ce49edc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java @@ -1,12 +1,12 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; - import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.Watcher; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; + public class CustomResourceEvent extends DefaultEvent { private final Watcher.Action action; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 05960e71f0..5c9f97621d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -1,9 +1,5 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; - import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -25,6 +21,10 @@ import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; + /** This is a special case since is not bound to a single custom resource */ public class CustomResourceEventSource> extends AbstractEventSource implements Watcher { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index cdfc979eed..9ad15e47f9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -1,17 +1,19 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; -import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import java.io.IOException; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; +import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; + public class TimerEventSource> extends AbstractEventSource { private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java index e5fb22ae62..717ceee07b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.processing.retry; -import io.javaoperatorsdk.operator.api.RetryInfo; import java.util.Optional; +import io.javaoperatorsdk.operator.api.RetryInfo; + public interface RetryExecution extends RetryInfo { /** diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java index bb3c66ea76..cfc390f9c7 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java @@ -1,9 +1,10 @@ package io.javaoperatorsdk.operator; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; -import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; class ControllerUtilsTest { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/CustomResourceUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/CustomResourceUtilsTest.java index 4443742259..3b05d81477 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/CustomResourceUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/CustomResourceUtilsTest.java @@ -1,10 +1,11 @@ package io.javaoperatorsdk.operator; -import io.javaoperatorsdk.operator.sample.simple.NamespacedTestCustomResource; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.sample.simple.NamespacedTestCustomResource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + public class CustomResourceUtilsTest { @Test public void assertClusterCustomResourceIsCluster() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index b5f1dce6c9..93e6e57492 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -1,12 +1,13 @@ package io.javaoperatorsdk.operator; +import java.util.HashMap; +import java.util.UUID; + import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionBuilder; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceSpec; -import java.util.HashMap; -import java.util.UUID; public class TestUtils { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java index 1b58b9e9e7..7e404e0223 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -1,9 +1,10 @@ package io.javaoperatorsdk.operator.api.config; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; class ControllerConfigurationTest { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java index 695a985ce0..33f8347f13 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java @@ -1,15 +1,5 @@ package io.javaoperatorsdk.operator.processing; -import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.util.Objects; import java.util.UUID; @@ -21,6 +11,16 @@ import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + class CustomResourceSelectorTest { public static final int SEPARATE_EXECUTION_TIMEOUT = 450; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index bba02b14fd..58ab81786c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -1,18 +1,5 @@ package io.javaoperatorsdk.operator.processing; -import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -33,6 +20,19 @@ import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + class DefaultEventHandlerTest { private static final Logger log = LoggerFactory.getLogger(DefaultEventHandlerTest.class); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java index 01688765dd..c9b414f52b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java @@ -1,13 +1,15 @@ package io.javaoperatorsdk.operator.processing; -import static org.assertj.core.api.Assertions.assertThat; - -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; import java.util.List; import java.util.UUID; + import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; + +import static org.assertj.core.api.Assertions.assertThat; + class EventBufferTest { private EventBuffer eventBuffer = new EventBuffer(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index 721aaaa7ef..f9611b0216 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -1,18 +1,5 @@ package io.javaoperatorsdk.operator.processing; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -36,6 +23,19 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + class EventDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java index f8f58e0aed..8862450d06 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java @@ -1,13 +1,5 @@ package io.javaoperatorsdk.operator.processing.event; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.Map; @@ -18,6 +10,14 @@ import io.javaoperatorsdk.operator.processing.DefaultEventHandler; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + class DefaultEventSourceManagerTest { public static final String CUSTOM_EVENT_SOURCE_NAME = "CustomEventSource"; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java index 7b5e8a7788..2f3dba65a5 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java @@ -1,12 +1,14 @@ package io.javaoperatorsdk.operator.processing.event; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; import java.util.Arrays; + import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + class EventListTest { @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 18a214e196..24eaa87b43 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -1,11 +1,5 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.time.LocalDateTime; import java.util.List; @@ -17,12 +11,18 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + class CustomResourceEventSourceTest { public static final String FINALIZER = "finalizer"; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index 74be731334..139f7bd0e9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -1,13 +1,18 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; +import java.text.ParseException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.awaitility.core.ConditionTimeoutException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; @@ -23,18 +28,15 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Version; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import java.text.ParseException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Date; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import org.awaitility.core.ConditionTimeoutException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; @EnableKubernetesMockClient(crud = true, https = false) public class CustomResourceSelectorTest { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index 62585f3255..892c422fc1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -1,24 +1,26 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import java.io.IOException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; + import org.awaitility.Awaitility; import org.awaitility.core.ConditionFactory; import org.awaitility.core.ThrowingRunnable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + class TimerEventSourceTest { public static final int INITIAL_DELAY = 50; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecutionTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecutionTest.java index 5934c70e58..0f8e44d1b2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecutionTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecutionTest.java @@ -1,11 +1,12 @@ package io.javaoperatorsdk.operator.processing.retry; -import static io.javaoperatorsdk.operator.processing.retry.GenericRetry.DEFAULT_INITIAL_INTERVAL; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Optional; + import org.junit.jupiter.api.Test; +import static io.javaoperatorsdk.operator.processing.retry.GenericRetry.DEFAULT_INITIAL_INTERVAL; +import static org.assertj.core.api.Assertions.assertThat; + public class GenericRetryExecutionTest { @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java index 0653024b33..0af533d0c3 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java @@ -1,5 +1,11 @@ package io.javaoperatorsdk.operator.sample.simple; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; @@ -10,10 +16,6 @@ import io.javaoperatorsdk.operator.api.DeleteControl; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; -import java.util.HashMap; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Controller(generationAwareEventProcessing = false) public class TestCustomResourceController implements ResourceController { diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 3a7ce8b0d2..06efcb3637 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -1,6 +1,20 @@ package io.javaoperatorsdk.operator.junit; -import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; @@ -18,20 +32,8 @@ import io.javaoperatorsdk.operator.api.config.Version; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.retry.Retry; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; + +import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override; public class OperatorExtension implements HasKubernetesClient, diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java index 4895832a03..279167fc22 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; + import javax.annotation.processing.ProcessingEnvironment; import javax.tools.StandardLocation; diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 194eed541a..363bae0c55 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,14 +1,15 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; public class AnnotationConfiguration implements ControllerConfiguration { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java index 1e367fc162..1258904394 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; + import org.apache.commons.lang3.ClassUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessor.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessor.java index 14b1307707..aaa55a01e8 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessor.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessor.java @@ -1,12 +1,7 @@ package io.javaoperatorsdk.operator.config.runtime; -import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.CONTROLLERS_RESOURCE_PATH; - -import com.google.auto.service.AutoService; -import com.squareup.javapoet.TypeName; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ResourceController; import java.util.Set; + import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; @@ -20,6 +15,14 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.ResourceController; + +import com.google.auto.service.AutoService; +import com.squareup.javapoet.TypeName; + +import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.CONTROLLERS_RESOURCE_PATH; + @SupportedAnnotationTypes("io.javaoperatorsdk.operator.api.Controller") @SupportedSourceVersion(SourceVersion.RELEASE_11) @AutoService(Processor.class) diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java index 87d8d710ab..913bab6624 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.util.Map; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.ResourceController; -import java.util.Map; public class RuntimeControllerMetadata { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/TypeParameterResolver.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/TypeParameterResolver.java index 503612c74d..8ef145d8b9 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/TypeParameterResolver.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/TypeParameterResolver.java @@ -1,12 +1,10 @@ package io.javaoperatorsdk.operator.config.runtime; -import static javax.lang.model.type.TypeKind.DECLARED; -import static javax.lang.model.type.TypeKind.TYPEVAR; - import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; + import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.DeclaredType; @@ -15,6 +13,9 @@ import javax.lang.model.type.TypeVariable; import javax.lang.model.util.Types; +import static javax.lang.model.type.TypeKind.DECLARED; +import static javax.lang.model.type.TypeKind.TYPEVAR; + /** This class can resolve a type parameter in the given index to the actual type defined. */ class TypeParameterResolver { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index bf06bc054a..6fa248a3d9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -1,22 +1,24 @@ package io.javaoperatorsdk.operator; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; -import io.javaoperatorsdk.operator.junit.OperatorExtension; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; -import io.javaoperatorsdk.operator.support.TestUtils; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; +import io.javaoperatorsdk.operator.support.TestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + public class ConcurrencyIT { public static final int NUMBER_OF_RESOURCES_CREATED = 50; public static final int NUMBER_OF_RESOURCES_DELETED = 30; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index b4bfe376f2..4855e61f49 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -1,7 +1,9 @@ package io.javaoperatorsdk.operator; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ConfigMap; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; @@ -9,9 +11,9 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; import io.javaoperatorsdk.operator.support.TestUtils; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class ControllerExecutionIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index 771c5c763b..d2954b86ee 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -1,7 +1,9 @@ package io.javaoperatorsdk.operator; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; @@ -10,9 +12,9 @@ import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResourceController; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResourceSpec; import io.javaoperatorsdk.operator.support.TestUtils; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class EventSourceIT { @RegisterExtension diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index b3b4be5f95..a68ba0bc7f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -1,7 +1,9 @@ package io.javaoperatorsdk.operator; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; @@ -12,9 +14,9 @@ import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResourceStatus; import io.javaoperatorsdk.operator.support.TestUtils; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class RetryIT { public static final int RETRY_INTERVAL = 150; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index ac7fa95c86..90f3d3caed 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator; -import io.javaoperatorsdk.operator.support.TestUtils; import java.util.Collections; import java.util.concurrent.TimeUnit; @@ -14,6 +13,7 @@ import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceController; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceStatus; +import io.javaoperatorsdk.operator.support.TestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index 9d4e0fd30c..7d513ddd33 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator; -import io.javaoperatorsdk.operator.support.TestUtils; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -13,6 +12,7 @@ import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceController; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceStatus; +import io.javaoperatorsdk.operator.support.TestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessorTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessorTest.java index ea26b91b93..e2f750b5d1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessorTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessorTest.java @@ -1,10 +1,11 @@ package io.javaoperatorsdk.operator.config.runtime; +import org.junit.jupiter.api.Test; + import com.google.testing.compile.Compilation; import com.google.testing.compile.CompilationSubject; import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; -import org.junit.jupiter.api.Test; class ControllerAnnotationProcessorTest { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 0252ae8ac1..56f1d8ee2e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -1,9 +1,12 @@ package io.javaoperatorsdk.operator.config.runtime; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.AppenderRef; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.test.appender.ListAppender; +import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; @@ -13,14 +16,11 @@ import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; -import io.javaoperatorsdk.operator.api.config.AbstractConfigurationService; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.AppenderRef; -import org.apache.logging.log4j.core.config.LoggerConfig; -import org.apache.logging.log4j.core.layout.PatternLayout; -import org.apache.logging.log4j.test.appender.ListAppender; -import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; public class DefaultConfigurationServiceTest { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java index 37dcec276c..74c313e87d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java @@ -1,12 +1,14 @@ package io.javaoperatorsdk.operator.sample.doubleupdate; -import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -import io.javaoperatorsdk.operator.api.*; import java.util.HashMap; import java.util.concurrent.atomic.AtomicInteger; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + @Controller public class DoubleUpdateTestCustomResourceController implements ResourceController, TestExecutionInfoProvider { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java index be4c404fda..f262440cae 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java @@ -1,17 +1,19 @@ package io.javaoperatorsdk.operator.sample.event; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; -import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @Controller public class EventSourceTestCustomResourceController diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java index 847f247872..9c5e363bc5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java @@ -1,15 +1,17 @@ package io.javaoperatorsdk.operator.sample.retry; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; -import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @Controller public class RetryTestCustomResourceController diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java index e26b629a74..4726a0fdfa 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java @@ -1,23 +1,25 @@ package io.javaoperatorsdk.operator.sample.simple; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.DeleteControl; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @Controller(generationAwareEventProcessing = false) public class TestCustomResourceController diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java index 8084ad7cfc..82157cf229 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java @@ -1,15 +1,17 @@ package io.javaoperatorsdk.operator.sample.subresource; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; -import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @Controller(generationAwareEventProcessing = false) public class SubResourceTestCustomResourceController diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java index 76ae1c4991..a81cf9ce05 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java @@ -1,11 +1,12 @@ package io.javaoperatorsdk.operator.support; +import java.util.HashMap; +import java.util.UUID; + import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceSpec; -import java.util.HashMap; -import java.util.UUID; public class TestUtils { diff --git a/pom.xml b/pom.xml index 901dd73ee4..9537e0b11a 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ 1.1.1 2.16.0 0.3.1 + 1.6.2 @@ -227,6 +228,26 @@ jandex-maven-plugin ${jandex-maven-plugin.version} + + net.revelc.code.formatter + formatter-maven-plugin + ${formatter-maven-plugin.version} + + .cache + + + + net.revelc.code + impsort-maven-plugin + ${impsort-maven-plugin.version} + + .cache + java.,javax.,org.,io.,com. + * + true + true + + @@ -274,7 +295,6 @@ net.revelc.code.formatter formatter-maven-plugin - ${formatter-maven-plugin.version} @@ -286,6 +306,18 @@ + + net.revelc.code + impsort-maven-plugin + + + sort + + sort + + + + diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java index 931f185acb..a4e8d66f45 100644 --- a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java +++ b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java @@ -1,5 +1,10 @@ package io.javaoperatorsdk.operator.sample; +import java.util.Collections; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.ServiceSpec; @@ -9,9 +14,6 @@ import io.javaoperatorsdk.operator.api.DeleteControl; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.UpdateControl; -import java.util.Collections; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** A very simple sample controller that creates a service with a label. */ @Controller diff --git a/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java b/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java index 7c5a997871..1b913575ab 100644 --- a/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java +++ b/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java @@ -1,13 +1,15 @@ package io.javaoperatorsdk.operator.sample; +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; -import java.util.List; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; @Configuration public class Config { From 8394f2df0f34beef4f24cbea05fe0e2a97133d40 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 17 Sep 2021 19:01:15 +0200 Subject: [PATCH 0053/1608] feat: use only one, configurable ExecutorService Fixes #540 --- .../io/javaoperatorsdk/operator/Operator.java | 13 ++++ .../api/config/ConfigurationService.java | 5 ++ .../api/config/ExecutorServiceProducer.java | 26 ++++++++ .../processing/DefaultEventHandler.java | 63 +++++++------------ .../processing/ExecutionConsumer.java | 25 -------- .../event/DefaultEventSourceManager.java | 4 -- 6 files changed, 67 insertions(+), 69 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionConsumer.java 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 63df0a79c3..dc6c2220e5 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 @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -102,6 +103,18 @@ public void close() { log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); + try { + log.debug("Closing executor"); + final var executor = configurationService.getExecutorService(); + executor.shutdown(); + if (!executor.awaitTermination(configurationService.getTerminationTimeoutSeconds(), + TimeUnit.SECONDS)) { + executor.shutdownNow(); // if we timed out, waiting, cancel everything + } + } catch (InterruptedException e) { + log.debug("Exception closing executor: {}", e.getLocalizedMessage()); + } + controllers.close(); k8sClient.close(); 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 d6075e75e5..c034f6bf03 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 @@ -8,6 +8,7 @@ import io.javaoperatorsdk.operator.api.ResourceController; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.concurrent.ExecutorService; /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { @@ -102,4 +103,8 @@ default int getTerminationTimeoutSeconds() { default Metrics getMetrics() { return Metrics.NOOP; } + + default ExecutorService getExecutorService() { + return ExecutorServiceProducer.getExecutor(concurrentReconciliationThreads()); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java new file mode 100644 index 0000000000..c3edfb2931 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicReference; + +class ExecutorServiceProducer { + + private final static AtomicReference executor = + new AtomicReference<>(); + + static ExecutorService getExecutor(int threadPoolSize) { + final var gotSet = + executor.compareAndSet(null, new ScheduledThreadPoolExecutor(threadPoolSize)); + final var result = executor.get(); + if (!gotSet) { + // check that we didn't try to change the pool size + if (result.getCorePoolSize() != threadPoolSize) { + throw new IllegalArgumentException( + "Cannot change the ExecutorService's thread pool size once set! Was " + + result.getCorePoolSize() + ", attempted to retrieve " + threadPoolSize); + } + } + return result; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index d2ef3d3416..2527ce294a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -44,61 +45,37 @@ public void failedEvent(String uid, Event event) {} private final EventBuffer eventBuffer; private final Set underProcessing = new HashSet<>(); - private final ScheduledThreadPoolExecutor executor; private final EventDispatcher eventDispatcher; private final Retry retry; private final Map retryState = new HashMap<>(); + private final ExecutorService executor; private final String controllerName; - private final int terminationTimeout; private final ReentrantLock lock = new ReentrantLock(); private DefaultEventSourceManager eventSourceManager; public DefaultEventHandler(ConfiguredController controller) { - this( - new EventDispatcher<>(controller), + this(controller.getConfiguration().getConfigurationService().getExecutorService(), controller.getConfiguration().getName(), - GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), - controller.getConfiguration().getConfigurationService().concurrentReconciliationThreads(), - controller.getConfiguration().getConfigurationService().getTerminationTimeoutSeconds()); + new EventDispatcher<>(controller), + GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration())); } - DefaultEventHandler(EventDispatcher dispatcher, String relatedControllerName, Retry retry) { - this( - dispatcher, - relatedControllerName, - retry, - ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER, - ConfigurationService.DEFAULT_TERMINATION_TIMEOUT_SECONDS); + DefaultEventHandler(EventDispatcher eventDispatcher, String relatedControllerName, + Retry retry) { + this(null, relatedControllerName, eventDispatcher, retry); } - private DefaultEventHandler( - EventDispatcher eventDispatcher, - String relatedControllerName, - Retry retry, - int concurrentReconciliationThreads, - int terminationTimeout) { + private DefaultEventHandler(ExecutorService executor, String relatedControllerName, + EventDispatcher eventDispatcher, Retry retry) { + this.executor = + executor == null + ? new ScheduledThreadPoolExecutor( + ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER) + : executor; + this.controllerName = relatedControllerName; this.eventDispatcher = eventDispatcher; this.retry = retry; - this.controllerName = relatedControllerName; eventBuffer = new EventBuffer(); - this.terminationTimeout = terminationTimeout; - executor = - new ScheduledThreadPoolExecutor( - concurrentReconciliationThreads, - runnable -> new Thread(runnable, "EventHandler-" + relatedControllerName)); - } - - @Override - public void close() { - try { - log.debug("Closing handler for {}", controllerName); - executor.shutdown(); - if (!executor.awaitTermination(terminationTimeout, TimeUnit.SECONDS)) { - executor.shutdownNow(); // if we timed out, waiting, cancel everything - } - } catch (InterruptedException e) { - log.debug("Exception closing handler for {}: {}", controllerName, e.getLocalizedMessage()); - } } public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { @@ -146,7 +123,13 @@ private void executeBufferedEvents(String customResourceUid) { latestCustomResource.get(), retryInfo(customResourceUid)); log.debug("Executing events for custom resource. Scope: {}", executionScope); - executor.execute(new ExecutionConsumer(executionScope, eventDispatcher, this)); + executor.execute(() -> { + // change thread name for easier debugging + Thread.currentThread().setName("EventHandler-" + controllerName); + PostExecutionControl postExecutionControl = + eventDispatcher.handleExecution(executionScope); + eventProcessingFinished(executionScope, postExecutionControl); + }); } else { log.debug( "Skipping executing controller for resource id: {}. Events in queue: {}." diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionConsumer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionConsumer.java deleted file mode 100644 index f648b82955..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionConsumer.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.javaoperatorsdk.operator.processing; - -import io.fabric8.kubernetes.client.CustomResource; - -class ExecutionConsumer> implements Runnable { - - private final ExecutionScope executionScope; - private final EventDispatcher eventDispatcher; - private final DefaultEventHandler defaultEventHandler; - - ExecutionConsumer( - ExecutionScope executionScope, - EventDispatcher eventDispatcher, - DefaultEventHandler defaultEventHandler) { - this.executionScope = executionScope; - this.eventDispatcher = eventDispatcher; - this.defaultEventHandler = defaultEventHandler; - } - - @Override - public void run() { - PostExecutionControl postExecutionControl = eventDispatcher.handleExecution(executionScope); - defaultEventHandler.eventProcessingFinished(executionScope, postExecutionControl); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index f71a4feb47..ff7ba37b15 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -51,10 +51,6 @@ public DefaultEventSourceManager(ConfiguredController controller) { new CustomResourceEventSource<>(controller)); } - public DefaultEventHandler getEventHandler() { - return defaultEventHandler; - } - @Override public void close() { try { From 7b6e88243f27126cb5146dace36099b41b79c0d4 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Sat, 18 Sep 2021 11:07:09 +0200 Subject: [PATCH 0054/1608] feat: introduce inner ControllerExecution to override toString This would allow easier diagnosis when the thread is rejected. --- .../api/config/ConfigurationService.java | 2 +- .../processing/DefaultEventHandler.java | 31 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) 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 c034f6bf03..9ae50156c3 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 @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.config; import java.util.Set; +import java.util.concurrent.ExecutorService; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; @@ -8,7 +9,6 @@ import io.javaoperatorsdk.operator.api.ResourceController; import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.concurrent.ExecutorService; /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 2527ce294a..00b4a2d972 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -7,7 +7,6 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; @@ -123,13 +122,7 @@ private void executeBufferedEvents(String customResourceUid) { latestCustomResource.get(), retryInfo(customResourceUid)); log.debug("Executing events for custom resource. Scope: {}", executionScope); - executor.execute(() -> { - // change thread name for easier debugging - Thread.currentThread().setName("EventHandler-" + controllerName); - PostExecutionControl postExecutionControl = - eventDispatcher.handleExecution(executionScope); - eventProcessingFinished(executionScope, postExecutionControl); - }); + executor.execute(new ControllerExecution(executionScope)); } else { log.debug( "Skipping executing controller for resource id: {}. Events in queue: {}." @@ -276,4 +269,26 @@ private void setUnderExecutionProcessing(String customResourceUid) { private void unsetUnderExecution(String customResourceUid) { underProcessing.remove(customResourceUid); } + + private class ControllerExecution implements Runnable { + private final ExecutionScope executionScope; + + private ControllerExecution(ExecutionScope executionScope) { + this.executionScope = executionScope; + } + + @Override + public void run() { + // change thread name for easier debugging + Thread.currentThread().setName("EventHandler-" + controllerName); + PostExecutionControl postExecutionControl = + eventDispatcher.handleExecution(executionScope); + eventProcessingFinished(executionScope, postExecutionControl); + } + + @Override + public String toString() { + return controllerName + " -> " + executionScope; + } + } } From 10525b8561615a3d6c08ef448a7849f34422781e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 21 Sep 2021 18:15:48 +0200 Subject: [PATCH 0055/1608] feat: attempt to figure out why thread pool is getting terminated --- .github/workflows/pr.yml | 9 ++-- .../api/config/ExecutorServiceProducer.java | 45 +++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 44fffbdeba..015484678f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,9 +15,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11, 16] - distribution: [ temurin, adopt-openj9 ] - kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] + # java: [11, 16] + java: [ 11 ] + distribution: [ temurin ] + kubernetes: [ 'v1.22.1' ] + # distribution: [ temurin, adopt-openj9 ] + # kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: - uses: actions/checkout@v2 - name: Set up Java and Maven diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java index c3edfb2931..5852e964c8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java @@ -1,17 +1,23 @@ package io.javaoperatorsdk.operator.api.config; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; class ExecutorServiceProducer { - private final static AtomicReference executor = + private final static AtomicReference executor = new AtomicReference<>(); static ExecutorService getExecutor(int threadPoolSize) { final var gotSet = - executor.compareAndSet(null, new ScheduledThreadPoolExecutor(threadPoolSize)); + executor.compareAndSet(null, new DebugThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, + TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>())); final var result = executor.get(); if (!gotSet) { // check that we didn't try to change the pool size @@ -23,4 +29,37 @@ static ExecutorService getExecutor(int threadPoolSize) { } return result; } + + private static class DebugThreadPoolExecutor extends ThreadPoolExecutor { + + public DebugThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + } + + public DebugThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, + ThreadFactory threadFactory) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); + } + + public DebugThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, + RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); + } + + public DebugThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, + TimeUnit unit, BlockingQueue workQueue, + ThreadFactory threadFactory, RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + } + + @Override + public void shutdown() { + Thread.dumpStack(); + super.shutdown(); + } + } } From 342176c39ac2dbba90af898b153d3abc1fbd9326 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 21 Sep 2021 21:04:40 +0200 Subject: [PATCH 0056/1608] feat: make debugging thread pool configurable --- .../api/config/ExecutorServiceProducer.java | 38 +++++-------------- .../operator/api/config/Utils.java | 11 +++++- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java index 5852e964c8..636bb34251 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java @@ -1,10 +1,7 @@ package io.javaoperatorsdk.operator.api.config; -import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -16,8 +13,7 @@ class ExecutorServiceProducer { static ExecutorService getExecutor(int threadPoolSize) { final var gotSet = - executor.compareAndSet(null, new DebugThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, - TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>())); + executor.compareAndSet(null, new InstrumentedExecutorService(threadPoolSize)); final var result = executor.get(); if (!gotSet) { // check that we didn't try to change the pool size @@ -30,35 +26,19 @@ static ExecutorService getExecutor(int threadPoolSize) { return result; } - private static class DebugThreadPoolExecutor extends ThreadPoolExecutor { + private static class InstrumentedExecutorService extends ThreadPoolExecutor { + private final boolean debug; - public DebugThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); - } - - public DebugThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, - TimeUnit unit, BlockingQueue workQueue, - ThreadFactory threadFactory) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); - } - - public DebugThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, - TimeUnit unit, BlockingQueue workQueue, - RejectedExecutionHandler handler) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); - } - - public DebugThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, - TimeUnit unit, BlockingQueue workQueue, - ThreadFactory threadFactory, RejectedExecutionHandler handler) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + public InstrumentedExecutorService(int corePoolSize) { + super(corePoolSize, corePoolSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); + debug = Utils.debugThreadPool(); } @Override public void shutdown() { - Thread.dumpStack(); + if (debug) { + Thread.dumpStack(); + } super.shutdown(); } } 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 ec1b6e3d72..1ea7285f89 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 @@ -13,6 +13,7 @@ public class Utils { private static final Logger log = LoggerFactory.getLogger(Utils.class); public static final String CHECK_CRD_ENV_KEY = "JAVA_OPERATOR_SDK_CHECK_CRD"; + public static final String DEBUG_THREAD_POOL_ENV_KEY = "JAVA_OPERATOR_SDK_DEBUG_THREAD_POOL"; /** * Attempts to load version information from a properties file produced at build time, currently @@ -60,7 +61,15 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - final var value = System.getProperty(CHECK_CRD_ENV_KEY); + return getBooleanEnvProperty(CHECK_CRD_ENV_KEY); + } + + private static boolean getBooleanEnvProperty(String envKey) { + final var value = System.getProperty(envKey); return value == null || Boolean.getBoolean(value); } + + public static boolean debugThreadPool() { + return getBooleanEnvProperty(DEBUG_THREAD_POOL_ENV_KEY); + } } From a86dcfa1ed0d9a7f8cfdee5e9f5762d5ae8c908b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 21 Sep 2021 22:02:59 +0200 Subject: [PATCH 0057/1608] refactor: encapsulate executor service access via ExecutorServiceManager The goal is to make sure the thread pool is properly started and shut down at appropriate time, while getting it ready for instrumentation. This should also address the tests issue where the executor was shut down at the end of one test and never re-started at the beginning of the next one thus leading to tasks being rejected. --- .../io/javaoperatorsdk/operator/Operator.java | 16 +- .../api/config/ConfigurationService.java | 3 +- .../api/config/ExecutorServiceManager.java | 144 ++++++++++++++++++ .../api/config/ExecutorServiceProducer.java | 45 ------ .../processing/DefaultEventHandler.java | 3 +- 5 files changed, 151 insertions(+), 60 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java 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 dc6c2220e5..a04f59c371 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 @@ -6,7 +6,6 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +16,7 @@ import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.DefaultEventHandler; import io.javaoperatorsdk.operator.processing.DefaultEventHandler.EventMonitor; @@ -94,6 +94,7 @@ public void start() { throw new OperatorException(error, e); } + ExecutorServiceManager.start(configurationService); controllers.start(); } @@ -103,20 +104,9 @@ public void close() { log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); - try { - log.debug("Closing executor"); - final var executor = configurationService.getExecutorService(); - executor.shutdown(); - if (!executor.awaitTermination(configurationService.getTerminationTimeoutSeconds(), - TimeUnit.SECONDS)) { - executor.shutdownNow(); // if we timed out, waiting, cancel everything - } - } catch (InterruptedException e) { - log.debug("Exception closing executor: {}", e.getLocalizedMessage()); - } - controllers.close(); + ExecutorServiceManager.instance().close(); k8sClient.close(); } 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 9ae50156c3..cb16793f43 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 @@ -2,6 +2,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; @@ -105,6 +106,6 @@ default Metrics getMetrics() { } default ExecutorService getExecutorService() { - return ExecutorServiceProducer.getExecutor(concurrentReconciliationThreads()); + return Executors.newFixedThreadPool(concurrentReconciliationThreads()); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java new file mode 100644 index 0000000000..ed8ee92cb4 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -0,0 +1,144 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.io.Closeable; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ExecutorServiceManager implements Closeable { + private static final Logger log = LoggerFactory.getLogger(ExecutorServiceManager.class); + private static ExecutorServiceManager instance; + + private final ExecutorService executor; + private final int terminationTimeoutSeconds; + + private ExecutorServiceManager(ExecutorService executor, int terminationTimeoutSeconds) { + this.executor = executor; + this.terminationTimeoutSeconds = terminationTimeoutSeconds; + } + + public static void start(ConfigurationService configuration) { + if (instance == null) { + instance = new ExecutorServiceManager( + new InstrumentedExecutorService(configuration.getExecutorService()), + configuration.getTerminationTimeoutSeconds()); + } else { + log.debug("Already started, reusing already setup instance!"); + } + } + + public static ExecutorServiceManager instance() { + if (instance == null) { + throw new IllegalStateException( + "ExecutorServiceManager hasn't been started. Call start method before using!"); + } + return instance; + } + + public ExecutorService executorService() { + return executor; + } + + @Override + public void close() { + try { + log.debug("Closing executor"); + executor.shutdown(); + if (!executor.awaitTermination(terminationTimeoutSeconds, TimeUnit.SECONDS)) { + executor.shutdownNow(); // if we timed out, waiting, cancel everything + } + } catch (InterruptedException e) { + log.debug("Exception closing executor: {}", e.getLocalizedMessage()); + } + } + + private static class InstrumentedExecutorService implements ExecutorService { + private final boolean debug; + private final ExecutorService executor; + + private InstrumentedExecutorService(ExecutorService executor) { + this.executor = executor; + debug = Utils.debugThreadPool(); + } + + @Override + public void shutdown() { + if (debug) { + Thread.dumpStack(); + } + executor.shutdown(); + } + + @Override + public List shutdownNow() { + return executor.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return executor.isShutdown(); + } + + @Override + public boolean isTerminated() { + return executor.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return executor.awaitTermination(timeout, unit); + } + + @Override + public Future submit(Callable task) { + return executor.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return executor.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + return executor.submit(task); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return executor.invokeAll(tasks); + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, + TimeUnit unit) throws InterruptedException { + return executor.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + return executor.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return executor.invokeAny(tasks, timeout, unit); + } + + @Override + public void execute(Runnable command) { + executor.execute(command); + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java deleted file mode 100644 index 636bb34251..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceProducer.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -class ExecutorServiceProducer { - - private final static AtomicReference executor = - new AtomicReference<>(); - - static ExecutorService getExecutor(int threadPoolSize) { - final var gotSet = - executor.compareAndSet(null, new InstrumentedExecutorService(threadPoolSize)); - final var result = executor.get(); - if (!gotSet) { - // check that we didn't try to change the pool size - if (result.getCorePoolSize() != threadPoolSize) { - throw new IllegalArgumentException( - "Cannot change the ExecutorService's thread pool size once set! Was " - + result.getCorePoolSize() + ", attempted to retrieve " + threadPoolSize); - } - } - return result; - } - - private static class InstrumentedExecutorService extends ThreadPoolExecutor { - private final boolean debug; - - public InstrumentedExecutorService(int corePoolSize) { - super(corePoolSize, corePoolSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); - debug = Utils.debugThreadPool(); - } - - @Override - public void shutdown() { - if (debug) { - Thread.dumpStack(); - } - super.shutdown(); - } - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 00b4a2d972..525293eee9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -16,6 +16,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -53,7 +54,7 @@ public void failedEvent(String uid, Event event) {} private DefaultEventSourceManager eventSourceManager; public DefaultEventHandler(ConfiguredController controller) { - this(controller.getConfiguration().getConfigurationService().getExecutorService(), + this(ExecutorServiceManager.instance().executorService(), controller.getConfiguration().getName(), new EventDispatcher<>(controller), GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration())); From 3494a6f03b15209745345eeedd61806c412d6fd1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 21 Sep 2021 23:11:19 +0200 Subject: [PATCH 0058/1608] fix: thread pool needs to be re-created if we restart after a close call This is done by moving the close method to static and nulling the singleton so that it can be re-created on next Operator start if needed. This is needed for proper testing support. --- .../io/javaoperatorsdk/operator/Operator.java | 4 ++-- .../api/config/ExecutorServiceManager.java | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) 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 a04f59c371..834c516c68 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 @@ -94,7 +94,7 @@ public void start() { throw new OperatorException(error, e); } - ExecutorServiceManager.start(configurationService); + ExecutorServiceManager.init(configurationService); controllers.start(); } @@ -106,7 +106,7 @@ public void close() { controllers.close(); - ExecutorServiceManager.instance().close(); + ExecutorServiceManager.close(); k8sClient.close(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index ed8ee92cb4..682b004c3d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.api.config; -import java.io.Closeable; import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; @@ -13,7 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ExecutorServiceManager implements Closeable { +public class ExecutorServiceManager { private static final Logger log = LoggerFactory.getLogger(ExecutorServiceManager.class); private static ExecutorServiceManager instance; @@ -25,7 +24,7 @@ private ExecutorServiceManager(ExecutorService executor, int terminationTimeoutS this.terminationTimeoutSeconds = terminationTimeoutSeconds; } - public static void start(ConfigurationService configuration) { + public static void init(ConfigurationService configuration) { if (instance == null) { instance = new ExecutorServiceManager( new InstrumentedExecutorService(configuration.getExecutorService()), @@ -35,6 +34,15 @@ public static void start(ConfigurationService configuration) { } } + public static void close() { + if (instance != null) { + instance.stop(); + } + // make sure that we remove the singleton so that the thread pool is re-created on next call to + // start + instance = null; + } + public static ExecutorServiceManager instance() { if (instance == null) { throw new IllegalStateException( @@ -47,8 +55,7 @@ public ExecutorService executorService() { return executor; } - @Override - public void close() { + private void stop() { try { log.debug("Closing executor"); executor.shutdown(); From e42c3d0711d046394822e8970d2badf50d9459e6 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 21 Sep 2021 23:26:56 +0200 Subject: [PATCH 0059/1608] feat: thread pool debug mode should be off by default --- .../io/javaoperatorsdk/operator/api/config/Utils.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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 1ea7285f89..b36c0468cd 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 @@ -61,15 +61,10 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return getBooleanEnvProperty(CHECK_CRD_ENV_KEY); - } - - private static boolean getBooleanEnvProperty(String envKey) { - final var value = System.getProperty(envKey); - return value == null || Boolean.getBoolean(value); + return Boolean.getBoolean(System.getProperty(CHECK_CRD_ENV_KEY, "true")); } public static boolean debugThreadPool() { - return getBooleanEnvProperty(DEBUG_THREAD_POOL_ENV_KEY); + return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); } } From 63c1a8d8ce6d0dc7b9ce6fa902a74de73d27c46e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 21 Sep 2021 23:29:17 +0200 Subject: [PATCH 0060/1608] fix: restore proper testing matrices (minus Java 16) --- .github/workflows/pr.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 015484678f..2f031c070e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,12 +15,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # java: [11, 16] java: [ 11 ] - distribution: [ temurin ] - kubernetes: [ 'v1.22.1' ] - # distribution: [ temurin, adopt-openj9 ] - # kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] + distribution: [ temurin, adopt-openj9 ] + kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: - uses: actions/checkout@v2 - name: Set up Java and Maven From 622922a40976424c5deb5a2fa5f39ae13ca0eab9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Sep 2021 03:14:30 +0000 Subject: [PATCH 0061/1608] chore(deps): bump junit-bom from 5.8.0 to 5.8.1 Bumps [junit-bom](https://github.com/junit-team/junit5) from 5.8.0 to 5.8.1. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.8.0...r5.8.1) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9537e0b11a..c326991396 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ ${java.version} ${java.version} - 5.8.0 + 5.8.1 5.7.2 1.7.32 2.14.1 From 69946045ce3da822a0d8974e5c39d338617fc73f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Sep 2021 03:14:35 +0000 Subject: [PATCH 0062/1608] chore(deps): bump directory-maven-plugin from 0.3.1 to 1.0 Bumps [directory-maven-plugin](https://github.com/jdcasey/directory-maven-plugin) from 0.3.1 to 1.0. - [Release notes](https://github.com/jdcasey/directory-maven-plugin/releases) - [Commits](https://github.com/jdcasey/directory-maven-plugin/compare/directory-maven-plugin-0.3.1...directory-maven-plugin-1.0) --- updated-dependencies: - dependency-name: org.commonjava.maven.plugins:directory-maven-plugin dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c326991396..db32e5c2cc 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 5.0.0 1.1.1 2.16.0 - 0.3.1 + 1.0 1.6.2 From 13ce5ef9acc1e5e709c1ca8b1a999f097c985fd7 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 21 Sep 2021 23:41:21 +0200 Subject: [PATCH 0063/1608] feat: monitor cache size --- .../io/javaoperatorsdk/operator/Metrics.java | 15 +++++++---- .../processing/CustomResourceCache.java | 25 ++++++++++--------- .../internal/CustomResourceEventSource.java | 5 ++-- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java index 0dd021b51f..31269b5d41 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator; +import java.util.Collections; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.ToDoubleFunction; import java.util.function.ToLongFunction; @@ -26,6 +28,7 @@ public class Metrics { public static final Metrics NOOP = new Metrics(new NoopMeterRegistry(Clock.SYSTEM)); + public static final String PREFIX = "operator.sdk."; private final MeterRegistry registry; public Metrics(MeterRegistry registry) { @@ -44,7 +47,7 @@ public interface ControllerExecution { public T timeControllerExecution(ControllerExecution execution) { final var name = execution.controllerName(); - final var execName = "operator.sdk.controllers.execution." + execution.name(); + final var execName = PREFIX + "controllers.execution." + execution.name(); final var timer = Timer.builder(execName) .tags("controller", name) @@ -68,25 +71,27 @@ public T timeControllerExecution(ControllerExecution execution) { } public void incrementControllerRetriesNumber() { - registry .counter( - "operator.sdk.retry.on.exception", "retry", "retryCounter", "type", + PREFIX + "retry.on.exception", "retry", "retryCounter", "type", "retryException") .increment(); } public void incrementProcessedEventsNumber() { - registry .counter( - "operator.sdk.total.events.received", "events", "totalEvents", "type", + PREFIX + "total.events.received", "events", "totalEvents", "type", "eventsReceived") .increment(); } + public > T monitorSizeOf(T map, String name) { + return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); + } + public static class NoopMeterRegistry extends MeterRegistry { public NoopMeterRegistry(Clock clock) { super(clock); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java index 74c832a323..a2a991fffd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java @@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.Metrics; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,34 +26,34 @@ public class CustomResourceCache { private static final Logger log = LoggerFactory.getLogger(CustomResourceCache.class); + private static final Predicate passthrough = o -> true; private final ObjectMapper objectMapper; - private final ConcurrentMap resources = new ConcurrentHashMap<>(); + private final ConcurrentMap resources; private final Lock lock = new ReentrantLock(); public CustomResourceCache() { - this(new ObjectMapper()); + this(new ObjectMapper(), Metrics.NOOP); } - public CustomResourceCache(ObjectMapper objectMapper) { + public CustomResourceCache(ObjectMapper objectMapper, Metrics metrics) { this.objectMapper = objectMapper; + resources = metrics.monitorSizeOf(new ConcurrentHashMap<>(), "cache"); } public void cacheResource(CustomResource resource) { - try { - lock.lock(); - resources.put(KubernetesResourceUtils.getUID(resource), resource); - } finally { - lock.unlock(); - } + cacheResource(resource, passthrough); } public void cacheResource(CustomResource resource, Predicate predicate) { try { lock.lock(); - if (predicate.test(resources.get(KubernetesResourceUtils.getUID(resource)))) { - log.trace("Update cache after condition is true: {}", getName(resource)); - resources.put(getUID(resource), resource); + final var uid = getUID(resource); + if (predicate.test(resources.get(uid))) { + if (passthrough != predicate) { + log.trace("Update cache after condition is true: {}", getName(resource)); + } + resources.put(uid, resource); } } finally { lock.unlock(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 5c9f97621d..50c02dee14 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -39,8 +39,9 @@ public class CustomResourceEventSource> extends A public CustomResourceEventSource(ConfiguredController controller) { this.controller = controller; this.watches = new LinkedList<>(); - this.customResourceCache = new CustomResourceCache( - controller.getConfiguration().getConfigurationService().getObjectMapper()); + final var configurationService = controller.getConfiguration().getConfigurationService(); + this.customResourceCache = new CustomResourceCache(configurationService.getObjectMapper(), + configurationService.getMetrics()); } @Override From ec61f5134f9812b636b540f3bf3cfd09a9caa5cc Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 22 Sep 2021 15:18:33 +0200 Subject: [PATCH 0064/1608] fix: make sure we always have a Metrics instance --- .../operator/processing/CustomResourceCache.java | 3 +++ .../event/internal/CustomResourceEventSourceTest.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java index a2a991fffd..75807acb84 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java @@ -38,6 +38,9 @@ public CustomResourceCache() { public CustomResourceCache(ObjectMapper objectMapper, Metrics metrics) { this.objectMapper = objectMapper; + if (metrics == null) { + metrics = Metrics.NOOP; + } resources = metrics.monitorSizeOf(new ConcurrentHashMap<>(), "cache"); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 24eaa87b43..c4b3533b5d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -10,6 +10,7 @@ import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; @@ -121,6 +122,7 @@ public TestConfiguration(boolean generationAware) { mock(ConfigurationService.class)); when(getConfigurationService().getObjectMapper()) .thenReturn(ConfigurationService.OBJECT_MAPPER); + when(getConfigurationService().getMetrics()).thenReturn(Metrics.NOOP); } } } From b6db6ab0c70590b836189ce3b365bd0f3b5370ee Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 23 Sep 2021 16:06:12 +0200 Subject: [PATCH 0065/1608] fix: only throw MissingCRDException if we get a 404 on the target CRD Fixes #552 --- .../operator/processing/EventDispatcher.java | 2 +- .../processing/event/DefaultEventSourceManager.java | 12 +++++++++--- .../event/DefaultEventSourceManagerTest.java | 4 +++- pom.xml | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 7ed0e25aca..79cf21bab9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -24,7 +24,7 @@ /** * Dispatches events to the Controller and handles Finalizers for a single type of Custom Resource. */ -class EventDispatcher> { +public class EventDispatcher> { private static final Logger log = LoggerFactory.getLogger(EventDispatcher.class); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index ff7ba37b15..842d89c659 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -35,18 +35,21 @@ public class DefaultEventSourceManager> private final Map eventSources = new ConcurrentHashMap<>(); private final DefaultEventHandler defaultEventHandler; private TimerEventSource retryTimerEventSource; + private final String targetCRDName; - DefaultEventSourceManager(DefaultEventHandler defaultEventHandler, boolean supportRetry) { + DefaultEventSourceManager(DefaultEventHandler defaultEventHandler, boolean supportRetry, + String targetCRDName) { this.defaultEventHandler = defaultEventHandler; defaultEventHandler.setEventSourceManager(this); if (supportRetry) { this.retryTimerEventSource = new TimerEventSource<>(); registerEventSource(RETRY_TIMER_EVENT_SOURCE_NAME, retryTimerEventSource); } + this.targetCRDName = targetCRDName; } public DefaultEventSourceManager(ConfiguredController controller) { - this(new DefaultEventHandler<>(controller), true); + this(new DefaultEventHandler<>(controller), true, controller.getConfiguration().getCRDName()); registerEventSource(CUSTOM_RESOURCE_EVENT_SOURCE_NAME, new CustomResourceEventSource<>(controller)); } @@ -92,7 +95,10 @@ public final void registerEventSource(String name, EventSource eventSource) if (e instanceof KubernetesClientException) { KubernetesClientException ke = (KubernetesClientException) e; if (404 == ke.getCode()) { - throw new MissingCRDException(null, null); + // only throw MissingCRDException if the 404 error occurs on the target CRD + if (targetCRDName.equals(ke.getFullResourceName())) { + throw new MissingCRDException(targetCRDName, null); + } } } throw new OperatorException("Couldn't register event source named '" + name + "'", e); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java index 8862450d06..1475195030 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java @@ -9,6 +9,7 @@ import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.DefaultEventHandler; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static org.assertj.core.api.Assertions.assertThat; @@ -24,7 +25,8 @@ class DefaultEventSourceManagerTest { private DefaultEventHandler defaultEventHandlerMock = mock(DefaultEventHandler.class); private DefaultEventSourceManager defaultEventSourceManager = - new DefaultEventSourceManager(defaultEventHandlerMock, false); + new DefaultEventSourceManager(defaultEventHandlerMock, false, + CustomResource.getCRDName(TestCustomResource.class)); @Test public void registersEventSource() { diff --git a/pom.xml b/pom.xml index db32e5c2cc..52edf21105 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ ${java.version} 5.8.1 - 5.7.2 + 5.8.0 1.7.32 2.14.1 3.12.4 From ccf305301d8f9e9e88f31cd492def656f3453241 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 23 Sep 2021 22:50:44 +0200 Subject: [PATCH 0066/1608] refactor: move check for missing CRD to CustomResourceEventSource This indeed makes more sense like this as this should only happen when attempting to register watches for the primary resources. --- .../event/DefaultEventSourceManager.java | 19 ++-------- .../internal/CustomResourceEventSource.java | 38 +++++++++++++------ .../event/DefaultEventSourceManagerTest.java | 4 +- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 842d89c659..3aad5131f8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -15,7 +15,6 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.KubernetesClientException; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.ConfiguredController; @@ -35,21 +34,18 @@ public class DefaultEventSourceManager> private final Map eventSources = new ConcurrentHashMap<>(); private final DefaultEventHandler defaultEventHandler; private TimerEventSource retryTimerEventSource; - private final String targetCRDName; - DefaultEventSourceManager(DefaultEventHandler defaultEventHandler, boolean supportRetry, - String targetCRDName) { + DefaultEventSourceManager(DefaultEventHandler defaultEventHandler, boolean supportRetry) { this.defaultEventHandler = defaultEventHandler; defaultEventHandler.setEventSourceManager(this); if (supportRetry) { this.retryTimerEventSource = new TimerEventSource<>(); registerEventSource(RETRY_TIMER_EVENT_SOURCE_NAME, retryTimerEventSource); } - this.targetCRDName = targetCRDName; } public DefaultEventSourceManager(ConfiguredController controller) { - this(new DefaultEventHandler<>(controller), true, controller.getConfiguration().getCRDName()); + this(new DefaultEventHandler<>(controller), true); registerEventSource(CUSTOM_RESOURCE_EVENT_SOURCE_NAME, new CustomResourceEventSource<>(controller)); } @@ -88,19 +84,10 @@ public final void registerEventSource(String name, EventSource eventSource) eventSource.setEventHandler(defaultEventHandler); eventSource.start(); } catch (Throwable e) { - if (e instanceof IllegalStateException) { + if (e instanceof IllegalStateException || e instanceof MissingCRDException) { // leave untouched throw e; } - if (e instanceof KubernetesClientException) { - KubernetesClientException ke = (KubernetesClientException) e; - if (404 == ke.getCode()) { - // only throw MissingCRDException if the 404 error occurs on the target CRD - if (targetCRDName.equals(ke.getFullResourceName())) { - throw new MissingCRDException(targetCRDName, null); - } - } - } throw new OperatorException("Couldn't register event source named '" + name + "'", e); } finally { lock.unlock(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 50c02dee14..b610c405c9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -11,10 +11,12 @@ import io.fabric8.kubernetes.api.model.ListOptions; import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.WatcherException; import io.fabric8.kubernetes.client.utils.Utils; +import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.CustomResourceCache; @@ -55,17 +57,31 @@ public void start() { options.setLabelSelector(labelSelector); } - if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { - var w = client.inAnyNamespace().watch(options, this); - watches.add(w); - log.debug("Registered {} -> {} for any namespace", controller, w); - } else { - targetNamespaces.forEach( - ns -> { - var w = client.inNamespace(ns).watch(options, this); - watches.add(w); - log.debug("Registered {} -> {} for namespace: {}", controller, w, ns); - }); + try { + if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { + var w = client.inAnyNamespace().watch(options, this); + watches.add(w); + log.debug("Registered {} -> {} for any namespace", controller, w); + } else { + targetNamespaces.forEach( + ns -> { + var w = client.inNamespace(ns).watch(options, this); + watches.add(w); + log.debug("Registered {} -> {} for namespace: {}", controller, w, ns); + }); + } + } catch (Exception e) { + if (e instanceof KubernetesClientException) { + KubernetesClientException ke = (KubernetesClientException) e; + if (404 == ke.getCode()) { + // only throw MissingCRDException if the 404 error occurs on the target CRD + final var targetCRDName = controller.getConfiguration().getCRDName(); + if (targetCRDName.equals(ke.getFullResourceName())) { + throw new MissingCRDException(targetCRDName, null); + } + } + } + throw e; } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java index 1475195030..8862450d06 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java @@ -9,7 +9,6 @@ import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.DefaultEventHandler; import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static org.assertj.core.api.Assertions.assertThat; @@ -25,8 +24,7 @@ class DefaultEventSourceManagerTest { private DefaultEventHandler defaultEventHandlerMock = mock(DefaultEventHandler.class); private DefaultEventSourceManager defaultEventSourceManager = - new DefaultEventSourceManager(defaultEventHandlerMock, false, - CustomResource.getCRDName(TestCustomResource.class)); + new DefaultEventSourceManager(defaultEventHandlerMock, false); @Test public void registersEventSource() { From 099ce066d0da613b9115008be0947eb1e4d05270 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Sep 2021 03:17:10 +0000 Subject: [PATCH 0067/1608] chore(deps): bump spring-boot.version from 2.5.4 to 2.5.5 Bumps `spring-boot.version` from 2.5.4 to 2.5.5. Updates `spring-boot-dependencies` from 2.5.4 to 2.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.5.4...v2.5.5) Updates `spring-boot-maven-plugin` from 2.5.4 to 2.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.5.4...v2.5.5) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 52edf21105..e994643f20 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 1.13.0 3.20.2 4.1.0 - 2.5.4 + 2.5.5 1.7.4 2.11 From 809aa0db6631e97591da924db70d9e5057360ce6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Sep 2021 09:56:54 +0200 Subject: [PATCH 0068/1608] chore(deps): bump assertj-core from 3.20.2 to 3.21.0 (#549) Bumps [assertj-core](https://github.com/assertj/assertj-core) from 3.20.2 to 3.21.0. - [Release notes](https://github.com/assertj/assertj-core/releases) - [Commits](https://github.com/assertj/assertj-core/compare/assertj-core-3.20.2...assertj-core-3.21.0) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e994643f20..5d42ee1fce 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ 1.0 0.19 1.13.0 - 3.20.2 + 3.21.0 4.1.0 2.5.5 1.7.4 From 4ac11bca09fe85c33c73a24be4bae6ad65cc6b70 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 24 Sep 2021 13:05:33 +0200 Subject: [PATCH 0069/1608] fix: restore backwards compatibility --- .../api/config/AbstractConfigurationService.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index 9983e2d6a6..1a37986d52 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -9,7 +9,7 @@ import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.api.ResourceController; -public abstract class AbstractConfigurationService implements ConfigurationService { +public class AbstractConfigurationService implements ConfigurationService { private final Map configurations = new ConcurrentHashMap<>(); private final Version version; @@ -60,8 +60,11 @@ public ControllerConfiguration getConfigurationFor return configuration; } - protected abstract void logMissingControllerWarning(String controllerKey, - String controllersNameMessage); + protected void logMissingControllerWarning(String controllerKey, + String controllersNameMessage) { + System.out + .println("Cannot find controller named '" + controllerKey + "'. " + controllersNameMessage); + } private String getControllersNameMessage() { return "Known controllers: " From 9012076d7bc18b5b4eb627403d3972e1d1793a6c Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 24 Sep 2021 10:06:47 +0200 Subject: [PATCH 0070/1608] fix: make it clear that `ConfiguredController.init` should not be called Fixes #554 --- .../operator/processing/ConfiguredController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index 31b06dd7af..dbac8b4d03 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -105,8 +105,7 @@ public UpdateControl execute() { @Override public void init(EventSourceManager eventSourceManager) { - this.manager = eventSourceManager; - controller.init(eventSourceManager); + throw new UnsupportedOperationException("This method should never be called directly"); } @Override From a9ddde0f13d5b35b284ed13615c0725d6a5ad4d9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 24 Sep 2021 11:13:32 +0000 Subject: [PATCH 0071/1608] Set new SNAPSHOT version into pom files. --- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- samples/common/pom.xml | 2 +- samples/pom.xml | 2 +- samples/pure-java/pom.xml | 2 +- samples/spring-boot-plain/pom.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 5449e14728..33ba79521f 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.7-SNAPSHOT + 1.9.8-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 926abceadc..e90ceede1e 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.7-SNAPSHOT + 1.9.8-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 90dff4f18d..b1f19d1b74 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.7-SNAPSHOT + 1.9.8-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 5d42ee1fce..baa171c9fd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.7-SNAPSHOT + 1.9.8-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/samples/common/pom.xml b/samples/common/pom.xml index 303f349e64..ca60b80c33 100644 --- a/samples/common/pom.xml +++ b/samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.7-SNAPSHOT + 1.9.8-SNAPSHOT operator-framework-samples-common diff --git a/samples/pom.xml b/samples/pom.xml index 912026ad6a..e08a1f54d7 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.7-SNAPSHOT + 1.9.8-SNAPSHOT java-operator-sdk-samples diff --git a/samples/pure-java/pom.xml b/samples/pure-java/pom.xml index 32d3f0adb4..a0af88e06b 100644 --- a/samples/pure-java/pom.xml +++ b/samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.7-SNAPSHOT + 1.9.8-SNAPSHOT operator-framework-samples-pure-java diff --git a/samples/spring-boot-plain/pom.xml b/samples/spring-boot-plain/pom.xml index 0cf16191d0..5dc5bd2b6b 100644 --- a/samples/spring-boot-plain/pom.xml +++ b/samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.7-SNAPSHOT + 1.9.8-SNAPSHOT operator-framework-samples-spring-boot-plain From 25e97d8c7da30af4c74c4e65502867ca716a930b Mon Sep 17 00:00:00 2001 From: csviri Date: Wed, 22 Sep 2021 22:57:43 +0200 Subject: [PATCH 0072/1608] Add InformerEventSource - this creates basically an adaptor between informers and event sources - add integration test serving as as a sample --- .../processing/event/DefaultEvent.java | 2 +- .../event/internal/InformerEvent.java | 48 +++++++++ .../event/internal/InformerEventSource.java | 98 +++++++++++++++++++ .../operator/junit/OperatorExtension.java | 4 + .../operator/InformerEventSourceIT.java | 79 +++++++++++++++ ...InformerEventSourceTestCustomResource.java | 18 ++++ ...entSourceTestCustomResourceController.java | 81 +++++++++++++++ ...rmerEventSourceTestCustomResourceSpec.java | 4 + ...erEventSourceTestCustomResourceStatus.java | 15 +++ 9 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceSpec.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceStatus.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java index fd48d1859c..b966c06609 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java @@ -50,7 +50,7 @@ public String toString() { + " }"; } - private static class UIDMatchingPredicate implements Predicate { + public static class UIDMatchingPredicate implements Predicate { private final String uid; public UIDMatchingPredicate(String uid) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java new file mode 100644 index 0000000000..ecfcece338 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java @@ -0,0 +1,48 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import java.util.Optional; +import java.util.function.Predicate; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.DefaultEvent; +import io.javaoperatorsdk.operator.processing.event.EventSource; + +public class InformerEvent extends DefaultEvent { + + private Action action; + private T resource; + private T oldResource; + + public InformerEvent(String relatedCustomResourceUid, EventSource eventSource, Action action, + T resource, + T oldResource) { + this(new UIDMatchingPredicate(relatedCustomResourceUid), eventSource, action, resource, + oldResource); + + } + + public InformerEvent(Predicate customResourcesSelector, EventSource eventSource, + Action action, + T resource, T oldResource) { + super(customResourcesSelector, eventSource); + this.action = action; + this.resource = resource; + this.oldResource = oldResource; + } + + public T getResource() { + return resource; + } + + public Optional getOldResource() { + return Optional.ofNullable(oldResource); + } + + public Action getAction() { + return action; + } + + public enum Action { + ADD, UPDATE, DELETE + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java new file mode 100644 index 0000000000..55946a14a4 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -0,0 +1,98 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import java.io.IOException; +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.fabric8.kubernetes.client.informers.SharedInformer; +import io.fabric8.kubernetes.client.informers.cache.Store; +import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; + +public class InformerEventSource extends AbstractEventSource { + + private final SharedInformer sharedInformer; + private final ResourceToRelatedCustomResourceUIDMapper mapper; + private final boolean skipUpdateEventPropagationIfNoChange; + + public InformerEventSource(SharedInformer sharedInformer, + ResourceToRelatedCustomResourceUIDMapper mapper) { + this(sharedInformer, mapper, true); + } + + InformerEventSource(KubernetesClient client, Class type, + ResourceToRelatedCustomResourceUIDMapper mapper) { + this(client, type, mapper, false); + } + + InformerEventSource(KubernetesClient client, Class type, + ResourceToRelatedCustomResourceUIDMapper mapper, + boolean skipUpdateEventPropagationIfNoChange) { + this(client.informers().sharedIndexInformerFor(type, 0), mapper, + skipUpdateEventPropagationIfNoChange); + } + + public InformerEventSource(SharedInformer sharedInformer, + ResourceToRelatedCustomResourceUIDMapper mapper, + boolean skipUpdateEventPropagationIfNoChange) { + this.sharedInformer = sharedInformer; + this.mapper = mapper; + this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; + + sharedInformer.addEventHandler(new ResourceEventHandler() { + @Override + public void onAdd(T t) { + propagateEvent(InformerEvent.Action.ADD, t, null); + } + + @Override + public void onUpdate(T oldObject, T newObject) { + if (InformerEventSource.this.skipUpdateEventPropagationIfNoChange && + oldObject.getMetadata().getResourceVersion() + .equals(newObject.getMetadata().getResourceVersion())) { + return; + } + propagateEvent(InformerEvent.Action.UPDATE, newObject, oldObject); + } + + @Override + public void onDelete(T t, boolean b) { + propagateEvent(InformerEvent.Action.DELETE, t, null); + } + }); + } + + private void propagateEvent(InformerEvent.Action action, T object, T oldObject) { + var uid = mapper.map(object); + if (uid.isEmpty()) { + return; + } + InformerEvent event = new InformerEvent(uid.get(), this, action, object, oldObject); + this.eventHandler.handleEvent(event); + } + + @Override + public void start() { + sharedInformer.run(); + } + + @Override + public void close() throws IOException { + sharedInformer.close(); + } + + public Store getStore() { + return sharedInformer.getStore(); + } + + public SharedInformer getSharedInformer() { + return sharedInformer; + } + + public interface ResourceToRelatedCustomResourceUIDMapper { + // in case cannot map to the related CR uid, skip the event processing + Optional map(T resource); + } + +} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 06efcb3637..a14ddb36a9 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -131,6 +131,10 @@ public T create(Class type, T resource) { return kubernetesClient.resources(type).inNamespace(namespace).create(resource); } + public T replace(Class type, T resource) { + return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); + } + @SuppressWarnings("unchecked") protected void before(ExtensionContext context) { namespace = context.getRequiredTestClass().getSimpleName(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java new file mode 100644 index 0000000000..66b9d73b98 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -0,0 +1,79 @@ +package io.javaoperatorsdk.operator; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +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.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResource; +import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResourceController; + +import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResourceController.RELATED_RESOURCE_UID; +import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResourceController.TARGET_CONFIG_MAP_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class InformerEventSourceIT { + + public static final String RESOURCE_NAME = "informertestcr"; + public static final String INITIAL_STATUS_MESSAGE = "Initial Status"; + public static final String UPDATE_STATUS_MESSAGE = "Updated Status"; + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withController(new InformerEventSourceTestCustomResourceController()) + .build(); + + @Test + public void testUsingInformerToWatchChangesOfConfigMap() { + var customResource = initialCustomResource(); + customResource = operator.create(InformerEventSourceTestCustomResource.class, customResource); + ConfigMap configMap = + operator.create(ConfigMap.class, relatedConfigMap(customResource.getMetadata().getUid())); + waitForCRStatusValue(INITIAL_STATUS_MESSAGE); + + configMap.getData().put(TARGET_CONFIG_MAP_KEY, UPDATE_STATUS_MESSAGE); + operator.replace(ConfigMap.class, configMap); + + waitForCRStatusValue(UPDATE_STATUS_MESSAGE); + } + + private ConfigMap relatedConfigMap(String relatedResourceAnnotation) { + ConfigMap configMap = new ConfigMap(); + + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName(RESOURCE_NAME); + objectMeta.setAnnotations(new HashMap<>()); + objectMeta.getAnnotations().put(RELATED_RESOURCE_UID, relatedResourceAnnotation); + configMap.setMetadata(objectMeta); + + configMap.setData(new HashMap<>()); + configMap.getData().put(TARGET_CONFIG_MAP_KEY, INITIAL_STATUS_MESSAGE); + return configMap; + } + + private InformerEventSourceTestCustomResource initialCustomResource() { + var customResource = new InformerEventSourceTestCustomResource(); + ObjectMeta objectMeta = new ObjectMeta(); + objectMeta.setName(RESOURCE_NAME); + customResource.setMetadata(objectMeta); + return customResource; + } + + private void waitForCRStatusValue(String value) { + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + var cr = + operator.getNamedResource(InformerEventSourceTestCustomResource.class, RESOURCE_NAME); + assertThat(cr.getStatus()).isNotNull(); + assertThat(cr.getStatus().getConfigMapValue()).isEqualTo(value); + }); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResource.java new file mode 100644 index 0000000000..fa7937e451 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResource.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.sample.informereventsource; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("Informereventsourcesample") +@ShortNames("ies") +public class InformerEventSourceTestCustomResource extends + CustomResource + implements Namespaced { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java new file mode 100644 index 0000000000..62a4631869 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java @@ -0,0 +1,81 @@ +package io.javaoperatorsdk.operator.sample.informereventsource; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.informers.SharedInformer; +import io.fabric8.kubernetes.client.informers.SharedInformerFactory; +import io.fabric8.kubernetes.client.informers.cache.Cache; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; +import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; + +import static io.javaoperatorsdk.operator.api.Controller.NO_FINALIZER; + +/** + * Copies the config map value from spec into status. The main purpose is to test and demonstrate + * sample usage of InformerEventSource + */ +@Controller(finalizerName = NO_FINALIZER) +public class InformerEventSourceTestCustomResourceController implements + ResourceController, KubernetesClientAware { + + private static final Logger LOGGER = + LoggerFactory.getLogger(InformerEventSourceTestCustomResourceController.class); + + public static final String RELATED_RESOURCE_UID = "relatedResourceUID"; + public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; + + private KubernetesClient kubernetesClient; + private SharedInformer informer; + + @Override + public void init(EventSourceManager eventSourceManager) { + SharedInformerFactory sharedInformerFactory = kubernetesClient.informers(); + informer = sharedInformerFactory.sharedIndexInformerFor(ConfigMap.class, 0); + eventSourceManager.registerEventSource("configmap", new InformerEventSource<>(informer, + resource -> { + if (resource.getMetadata() == null || resource.getMetadata().getAnnotations() == null) { + return Optional.empty(); + } + return Optional + .ofNullable(resource.getMetadata().getAnnotations().get(RELATED_RESOURCE_UID)); + })); + } + + @Override + public UpdateControl createOrUpdateResource( + InformerEventSourceTestCustomResource resource, + Context context) { + + // Reading the config map from the informer not from the API + // name of the config map same as custom resource for sake of simplicity + ConfigMap configMap = + informer.getStore().getByKey(Cache.namespaceKeyFunc(resource.getMetadata().getNamespace(), + resource.getMetadata().getName())); + + String targetStatus = configMap.getData().get(TARGET_CONFIG_MAP_KEY); + LOGGER.debug("Setting target status for CR: {}", targetStatus); + resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); + resource.getStatus().setConfigMapValue(targetStatus); + return UpdateControl.updateStatusSubResource(resource); + } + + @Override + public KubernetesClient getKubernetesClient() { + return kubernetesClient; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceSpec.java new file mode 100644 index 0000000000..a84800d6d0 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceSpec.java @@ -0,0 +1,4 @@ +package io.javaoperatorsdk.operator.sample.informereventsource; + +public class InformerEventSourceTestCustomResourceSpec { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceStatus.java new file mode 100644 index 0000000000..b4b6b93958 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceStatus.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.informereventsource; + +public class InformerEventSourceTestCustomResourceStatus { + + private String configMapValue; + + public String getConfigMapValue() { + return configMapValue; + } + + public InformerEventSourceTestCustomResourceStatus setConfigMapValue(String configMapValue) { + this.configMapValue = configMapValue; + return this; + } +} From 10b79cdf5dd25cb9eb4add3c02b903e81dcb0ad4 Mon Sep 17 00:00:00 2001 From: csviri Date: Tue, 28 Sep 2021 14:06:32 +0200 Subject: [PATCH 0073/1608] Updated informer mapper --- .../event/internal/InformerEventSource.java | 15 ++++++++------- ...erEventSourceTestCustomResourceController.java | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 55946a14a4..4d91d65459 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing.event.internal; import java.io.IOException; -import java.util.Optional; +import java.util.List; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; @@ -64,12 +64,14 @@ public void onDelete(T t, boolean b) { } private void propagateEvent(InformerEvent.Action action, T object, T oldObject) { - var uid = mapper.map(object); - if (uid.isEmpty()) { + var uids = mapper.map(object); + if (uids.isEmpty()) { return; } - InformerEvent event = new InformerEvent(uid.get(), this, action, object, oldObject); - this.eventHandler.handleEvent(event); + uids.forEach(uid -> { + InformerEvent event = new InformerEvent(uid, this, action, object, oldObject); + this.eventHandler.handleEvent(event); + }); } @Override @@ -91,8 +93,7 @@ public SharedInformer getSharedInformer() { } public interface ResourceToRelatedCustomResourceUIDMapper { - // in case cannot map to the related CR uid, skip the event processing - Optional map(T resource); + List map(T resource); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java index 62a4631869..99624eee59 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.sample.informereventsource; -import java.util.Optional; +import java.util.Arrays; +import java.util.Collections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,10 +45,9 @@ public void init(EventSourceManager eventSourceManager) { eventSourceManager.registerEventSource("configmap", new InformerEventSource<>(informer, resource -> { if (resource.getMetadata() == null || resource.getMetadata().getAnnotations() == null) { - return Optional.empty(); + return Collections.emptyList(); } - return Optional - .ofNullable(resource.getMetadata().getAnnotations().get(RELATED_RESOURCE_UID)); + return Arrays.asList(resource.getMetadata().getAnnotations().get(RELATED_RESOURCE_UID)); })); } From 327e93a4b33ccfe3be5e3546bbb04b3c82d5116b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 28 Sep 2021 17:55:44 +0200 Subject: [PATCH 0074/1608] refactor: remove spec class since it's not really needed --- .../InformerEventSourceTestCustomResource.java | 2 +- .../InformerEventSourceTestCustomResourceSpec.java | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceSpec.java diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResource.java index fa7937e451..ff1c6758bb 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResource.java @@ -12,7 +12,7 @@ @Kind("Informereventsourcesample") @ShortNames("ies") public class InformerEventSourceTestCustomResource extends - CustomResource + CustomResource implements Namespaced { } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceSpec.java deleted file mode 100644 index a84800d6d0..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceSpec.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.javaoperatorsdk.operator.sample.informereventsource; - -public class InformerEventSourceTestCustomResourceSpec { -} From 0494505de8dcbd14e67ef2a803803b14d9a3ede2 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 29 Sep 2021 09:13:09 +0200 Subject: [PATCH 0075/1608] Improvements (#569) * feat: add Mappers class to provide convenient mapper implementations * feat: add getAssociated method on InformerEventSource Actually not sure if this makes sense: could there be cases where a given primary resource maps to several secondary resources? * refactor: use Function instead of dedicated classes. Clearer? --- .../event/internal/InformerEventSource.java | 54 ++++++++++++------- .../processing/event/internal/Mappers.java | 32 +++++++++++ ...entSourceTestCustomResourceController.java | 25 +++------ 3 files changed, 74 insertions(+), 37 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 4d91d65459..37e1add66e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -1,46 +1,57 @@ package io.javaoperatorsdk.operator.processing.event.internal; import java.io.IOException; -import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedInformer; +import io.fabric8.kubernetes.client.informers.cache.Cache; import io.fabric8.kubernetes.client.informers.cache.Store; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; public class InformerEventSource extends AbstractEventSource { private final SharedInformer sharedInformer; - private final ResourceToRelatedCustomResourceUIDMapper mapper; + private final Function> resourceToUIDs; + private final Function associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; public InformerEventSource(SharedInformer sharedInformer, - ResourceToRelatedCustomResourceUIDMapper mapper) { - this(sharedInformer, mapper, true); + Function> resourceToUIDs) { + this(sharedInformer, resourceToUIDs, null, true); } - InformerEventSource(KubernetesClient client, Class type, - ResourceToRelatedCustomResourceUIDMapper mapper) { - this(client, type, mapper, false); + public InformerEventSource(KubernetesClient client, Class type, + Function> resourceToUIDs) { + this(client, type, resourceToUIDs, false); } InformerEventSource(KubernetesClient client, Class type, - ResourceToRelatedCustomResourceUIDMapper mapper, + Function> resourceToUIDs, boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), mapper, + this(client.informers().sharedIndexInformerFor(type, 0), resourceToUIDs, null, skipUpdateEventPropagationIfNoChange); } public InformerEventSource(SharedInformer sharedInformer, - ResourceToRelatedCustomResourceUIDMapper mapper, + Function> resourceToUIDs, + Function associatedWith, boolean skipUpdateEventPropagationIfNoChange) { this.sharedInformer = sharedInformer; - this.mapper = mapper; + this.resourceToUIDs = resourceToUIDs; this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; - sharedInformer.addEventHandler(new ResourceEventHandler() { + this.associatedWith = Objects.requireNonNullElseGet(associatedWith, () -> cr -> { + final var metadata = cr.getMetadata(); + return getStore().getByKey(Cache.namespaceKeyFunc(metadata.getNamespace(), + metadata.getName())); + }); + + sharedInformer.addEventHandler(new ResourceEventHandler<>() { @Override public void onAdd(T t) { propagateEvent(InformerEvent.Action.ADD, t, null); @@ -64,7 +75,7 @@ public void onDelete(T t, boolean b) { } private void propagateEvent(InformerEvent.Action action, T object, T oldObject) { - var uids = mapper.map(object); + var uids = resourceToUIDs.apply(object); if (uids.isEmpty()) { return; } @@ -88,12 +99,19 @@ public Store getStore() { return sharedInformer.getStore(); } - public SharedInformer getSharedInformer() { - return sharedInformer; + /** + * Retrieves the informed resource associated with the specified primary resource as defined by + * the function provided when this InformerEventSource was created + * + * @param resource the primary resource we want to retrieve the associated resource for + * @return the informed resource associated with the specified primary resource + */ + public T getAssociated(HasMetadata resource) { + return associatedWith.apply(resource); } - public interface ResourceToRelatedCustomResourceUIDMapper { - List map(T resource); - } + public SharedInformer getSharedInformer() { + return sharedInformer; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java new file mode 100644 index 0000000000..dc0b4b4501 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java @@ -0,0 +1,32 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import java.util.Collections; +import java.util.Set; +import java.util.function.Function; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class Mappers { + public static Function> fromAnnotation( + String annotationKey) { + return fromMetadata(annotationKey, false); + } + + public static Function> fromLabel( + String labelKey) { + return fromMetadata(labelKey, true); + } + + private static Function> fromMetadata( + String key, boolean isLabel) { + return resource -> { + final var metadata = resource.getMetadata(); + if (metadata == null) { + return Collections.emptySet(); + } else { + final var map = isLabel ? metadata.getLabels() : metadata.getAnnotations(); + return map != null ? Set.of(map.get(key)) : Collections.emptySet(); + } + }; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java index 99624eee59..e5c44198b4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java @@ -1,16 +1,10 @@ package io.javaoperatorsdk.operator.sample.informereventsource; -import java.util.Arrays; -import java.util.Collections; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.informers.SharedInformer; -import io.fabric8.kubernetes.client.informers.SharedInformerFactory; -import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; @@ -18,6 +12,7 @@ import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.internal.Mappers; import static io.javaoperatorsdk.operator.api.Controller.NO_FINALIZER; @@ -36,19 +31,13 @@ public class InformerEventSourceTestCustomResourceController implements public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; private KubernetesClient kubernetesClient; - private SharedInformer informer; + private InformerEventSource eventSource; @Override public void init(EventSourceManager eventSourceManager) { - SharedInformerFactory sharedInformerFactory = kubernetesClient.informers(); - informer = sharedInformerFactory.sharedIndexInformerFor(ConfigMap.class, 0); - eventSourceManager.registerEventSource("configmap", new InformerEventSource<>(informer, - resource -> { - if (resource.getMetadata() == null || resource.getMetadata().getAnnotations() == null) { - return Collections.emptyList(); - } - return Arrays.asList(resource.getMetadata().getAnnotations().get(RELATED_RESOURCE_UID)); - })); + eventSource = new InformerEventSource<>(kubernetesClient, ConfigMap.class, + Mappers.fromAnnotation(RELATED_RESOURCE_UID)); + eventSourceManager.registerEventSource("configmap", eventSource); } @Override @@ -58,9 +47,7 @@ public UpdateControl createOrUpdateResour // Reading the config map from the informer not from the API // name of the config map same as custom resource for sake of simplicity - ConfigMap configMap = - informer.getStore().getByKey(Cache.namespaceKeyFunc(resource.getMetadata().getNamespace(), - resource.getMetadata().getName())); + ConfigMap configMap = eventSource.getAssociated(resource); String targetStatus = configMap.getData().get(TARGET_CONFIG_MAP_KEY); LOGGER.debug("Setting target status for CR: {}", targetStatus); From 6027f9140fb950d496f1d4efea6c794589992ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 29 Sep 2021 12:11:54 +0200 Subject: [PATCH 0076/1608] Convinient API to re-schedule events for UpdateControl (#568) --- .../operator/api/UpdateControl.java | 17 ++++++++++++++ .../processing/DefaultEventHandler.java | 8 +++++++ .../operator/processing/EventDispatcher.java | 17 ++++++++++---- .../processing/PostExecutionControl.java | 13 +++++++++-- .../processing/DefaultEventHandlerTest.java | 23 +++++++++++-------- .../processing/EventDispatcherTest.java | 14 +++++++++++ 6 files changed, 75 insertions(+), 17 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java index 09bde6856a..9dea38aea0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java @@ -1,5 +1,8 @@ package io.javaoperatorsdk.operator.api; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + import io.fabric8.kubernetes.client.CustomResource; public class UpdateControl { @@ -7,6 +10,7 @@ public class UpdateControl { private final T customResource; private final boolean updateStatusSubResource; private final boolean updateCustomResource; + private Long reScheduleDelay = null; private UpdateControl( T customResource, boolean updateStatusSubResource, boolean updateCustomResource) { @@ -40,6 +44,19 @@ public static UpdateControl noUpdate() { return new UpdateControl<>(null, false, false); } + public UpdateControl withReSchedule(long delay, TimeUnit timeUnit) { + return withReSchedule(timeUnit.toMillis(delay)); + } + + public UpdateControl withReSchedule(long delay) { + this.reScheduleDelay = delay; + return this; + } + + public Optional getReScheduleDelay() { + return Optional.ofNullable(reScheduleDelay); + } + public T getCustomResource() { return customResource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 525293eee9..74d9e994de 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -163,6 +163,7 @@ void eventProcessingFinished( cleanupAfterDeletedEvent(executionScope.getCustomResourceUid()); } else { cacheUpdatedResourceIfChanged(executionScope, postExecutionControl); + reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getCustomResource()); executeBufferedEvents(executionScope.getCustomResourceUid()); } } finally { @@ -170,6 +171,13 @@ void eventProcessingFinished( } } + private void reScheduleExecutionIfInstructed(PostExecutionControl postExecutionControl, + R customResource) { + postExecutionControl.getReScheduleDelay().ifPresent(delay -> eventSourceManager + .getRetryTimerEventSource() + .scheduleOnce(customResource, delay)); + } + /** * Regarding the events there are 2 approaches we can take. Either retry always when there are new * events (received meanwhile retry is in place or already in buffer) instantly or always wait diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 79cf21bab9..85fcbefb9d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -140,13 +140,20 @@ private PostExecutionControl handleCreateOrUpdate( } else if (updateControl.isUpdateCustomResource()) { updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); } + return createPostExecutionControl(updatedCustomResource, updateControl); + } + } - if (updatedCustomResource != null) { - return PostExecutionControl.customResourceUpdated(updatedCustomResource); - } else { - return PostExecutionControl.defaultDispatch(); - } + private PostExecutionControl createPostExecutionControl(R updatedCustomResource, + UpdateControl updateControl) { + PostExecutionControl postExecutionControl; + if (updatedCustomResource != null) { + postExecutionControl = PostExecutionControl.customResourceUpdated(updatedCustomResource); + } else { + postExecutionControl = PostExecutionControl.defaultDispatch(); } + updateControl.getReScheduleDelay().ifPresent(postExecutionControl::withReSchedule); + return postExecutionControl; } private PostExecutionControl handleDelete(R resource, Context context) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java index b36bcedd90..763e01c181 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java @@ -7,11 +7,11 @@ public final class PostExecutionControl> { private final boolean onlyFinalizerHandled; - private final R updatedCustomResource; - private final RuntimeException runtimeException; + private Long reScheduleDelay = null; + private PostExecutionControl( boolean onlyFinalizerHandled, R updatedCustomResource, @@ -54,10 +54,19 @@ public boolean exceptionDuringExecution() { return runtimeException != null; } + public PostExecutionControl withReSchedule(long delay) { + this.reScheduleDelay = delay; + return this; + } + public Optional getRuntimeException() { return Optional.ofNullable(runtimeException); } + public Optional getReScheduleDelay() { + return Optional.ofNullable(reScheduleDelay); + } + @Override public String toString() { return "PostExecutionControl{" diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 58ab81786c..55e7a9cc54 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -23,15 +23,7 @@ import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class DefaultEventHandlerTest { @@ -187,7 +179,6 @@ public void successfulExecutionResetsTheRetry() { Event event = prepareCREvent(); TestCustomResource customResource = testCustomResource(); customResource.getMetadata().setUid(event.getRelatedCustomResourceUid()); - ExecutionScope executionScope = new ExecutionScope(Arrays.asList(event), customResource, null); PostExecutionControl postExecutionControlWithException = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); PostExecutionControl defaultDispatchControl = PostExecutionControl.defaultDispatch(); @@ -222,6 +213,18 @@ public void successfulExecutionResetsTheRetry() { assertThat(executionScopes.get(1).getRetryInfo().isLastAttempt()).isEqualTo(false); } + @Test + public void scheduleTimedEventIfInstructedByPostExecutionControl() { + var testDelay = 10000l; + when(eventDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); + + defaultEventHandler.handleEvent(prepareCREvent()); + + verify(retryTimerEventSourceMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) + .scheduleOnce(any(), eq(testDelay)); + } + private void waitMinimalTime() { try { Thread.sleep(50); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index f9611b0216..2f9de4de82 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -289,6 +289,20 @@ public boolean isLastAttempt() { assertThat(retryInfo.isLastAttempt()).isEqualTo(true); } + @Test + void setReScheduleToPostExecutionControlFromUpdateControl() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + + when(controller.createOrUpdateResource(eq(testCustomResource), any())) + .thenReturn( + UpdateControl.updateStatusSubResource(testCustomResource).withReSchedule(1000l)); + + PostExecutionControl control = eventDispatcher.handleExecution( + executionScopeWithCREvent(Watcher.Action.ADDED, testCustomResource)); + + assertThat(control.getReScheduleDelay().get()).isEqualTo(1000l); + } + private void markForDeletion(CustomResource customResource) { customResource.getMetadata().setDeletionTimestamp("2019-8-10"); } From 6cf4da0f4c52219f71c1678058e8dff5ebf7ed52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 29 Sep 2021 15:52:30 +0200 Subject: [PATCH 0077/1608] Improvements on Test Flakyness (#572) --- .../processing/retry/GenericRetryExecution.java | 4 ++-- .../processing/DefaultEventHandlerTest.java | 15 +++++++-------- .../operator/junit/OperatorExtension.java | 5 +++++ .../operator/SubResourceUpdateIT.java | 6 +++--- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecution.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecution.java index a2c7a9a609..db06363ced 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecution.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecution.java @@ -6,8 +6,8 @@ public class GenericRetryExecution implements RetryExecution { private final GenericRetry genericRetry; - private int lastAttemptIndex = 0; - private long currentInterval; + private volatile int lastAttemptIndex = 0; + private volatile long currentInterval; public GenericRetryExecution(GenericRetry genericRetry) { this.genericRetry = genericRetry; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 55e7a9cc54..591c7fa1e0 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -152,14 +152,14 @@ public void executesTheControllerInstantlyAfterErrorIfEventsBuffered() { PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); + when(eventDispatcherMock.handleExecution(any())) + .thenReturn(postExecutionControl) + .thenReturn(PostExecutionControl.defaultDispatch()); + // start processing an event defaultEventHandlerWithRetry.handleEvent(event); - // buffer an another event + // buffer another event defaultEventHandlerWithRetry.handleEvent(event); - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) - .handleExecution(any()); - - defaultEventHandlerWithRetry.eventProcessingFinished(executionScope, postExecutionControl); ArgumentCaptor executionScopeArgumentCaptor = ArgumentCaptor.forClass(ExecutionScope.class); @@ -191,15 +191,14 @@ public void successfulExecutionResetsTheRetry() { ArgumentCaptor.forClass(ExecutionScope.class); defaultEventHandlerWithRetry.handleEvent(event); - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) .handleExecution(any()); - defaultEventHandlerWithRetry.handleEvent(event); + defaultEventHandlerWithRetry.handleEvent(event); verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) .handleExecution(any()); - defaultEventHandlerWithRetry.handleEvent(event); + defaultEventHandlerWithRetry.handleEvent(event); verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(3)) .handleExecution(executionScopeArgumentCaptor.capture()); log.info("Finished successfulExecutionResetsTheRetry"); diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index a14ddb36a9..743e2c7b72 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -135,6 +135,11 @@ public T replace(Class type, T resource) { return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); } + public T get(Class type, String name) { + return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); + } + + @SuppressWarnings("unchecked") protected void before(ExtensionContext context) { namespace = context.getRequiredTestClass().getSimpleName(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 90f3d3caed..c09982b639 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -12,9 +12,9 @@ import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResource; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceController; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceSpec; -import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceStatus; import io.javaoperatorsdk.operator.support.TestUtils; +import static io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceStatus.State.SUCCESS; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -87,7 +87,7 @@ public void updateCustomResourceAfterSubResourceChange() { awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(200); + waitXms(500); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(3); @@ -104,7 +104,7 @@ void awaitStatusUpdated(String name) { assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); assertThat(cr.getStatus().getState()) - .isEqualTo(SubResourceTestCustomResourceStatus.State.SUCCESS); + .isEqualTo(SUCCESS); }); } From 0dc0de170940acac41f0245fd28e3a1d17158cb5 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Wed, 29 Sep 2021 16:22:53 +0200 Subject: [PATCH 0078/1608] Custom event filter for controllers (#457) Custom event filter for controllers --- .../operator/api/Controller.java | 13 +- .../AbstractControllerConfiguration.java | 4 +- .../api/config/ControllerConfiguration.java | 15 ++ .../ControllerConfigurationOverrider.java | 10 ++ .../DefaultControllerConfiguration.java | 25 ++- .../processing/CustomResourceCache.java | 23 +-- .../internal/CustomResourceEventFilter.java | 58 +++++++ .../internal/CustomResourceEventFilters.java | 163 ++++++++++++++++++ .../internal/CustomResourceEventSource.java | 63 +++---- .../CustomResourceEventFilterTest.java | 159 +++++++++++++++++ .../CustomResourceEventSourceTest.java | 15 +- .../internal/CustomResourceSelectorTest.java | 2 +- .../sample/simple/TestCustomResource.java | 10 ++ .../runtime/AnnotationConfiguration.java | 29 ++++ 14 files changed, 528 insertions(+), 61 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java index 4c332ebf24..ebf9c65d42 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java @@ -5,6 +5,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface Controller { @@ -46,7 +48,16 @@ * upon. The label selector can be made of multiple comma separated requirements that acts as a * logical AND operator. * - * @return the finalizer name + * @return the label selector */ String labelSelector() default NULL; + + + /** + * Optional list of classes providing custom {@link CustomResourceEventFilter}. + * + * @return the list of event filters. + */ + @SuppressWarnings("rawtypes") + Class[] eventFilters() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java index e3780fc73e..a3ba836922 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java @@ -3,6 +3,7 @@ import java.util.Set; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; /** * @deprecated use {@link DefaultControllerConfiguration} instead @@ -25,9 +26,10 @@ public AbstractControllerConfiguration(String associatedControllerClassName, Str String crdName, String finalizer, boolean generationAware, Set namespaces, RetryConfiguration retryConfiguration, String labelSelector, + CustomResourceEventFilter customResourcePredicate, Class customResourceClass, ConfigurationService service) { super(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, - retryConfiguration, labelSelector, customResourceClass, service); + retryConfiguration, labelSelector, customResourcePredicate, customResourceClass, service); } } 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 bcc068cec6..b9bcad0290 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 @@ -7,6 +7,8 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters; public interface ControllerConfiguration { @@ -98,4 +100,17 @@ default void setConfigurationService(ConfigurationService service) {} default boolean useFinalizer() { return !Controller.NO_FINALIZER.equals(getFinalizer()); } + + /** + * Allow controllers to filter events before they are provided to the + * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. + *

+ * Note that the provided filter is combined with {@link #isGenerationAware()} to compute the + * final set of fiolters that should be applied; + * + * @return + */ + default CustomResourceEventFilter getEventFilter() { + return CustomResourceEventFilters.passthrough(); + } } 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 a199c81157..f85870ac76 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 @@ -5,6 +5,7 @@ import java.util.Set; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; public class ControllerConfigurationOverrider> { @@ -13,6 +14,7 @@ public class ControllerConfigurationOverrider> { private final Set namespaces; private RetryConfiguration retry; private String labelSelector; + private CustomResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private ControllerConfigurationOverrider(ControllerConfiguration original) { @@ -21,6 +23,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { namespaces = new HashSet<>(original.getNamespaces()); retry = original.getRetryConfiguration(); labelSelector = original.getLabelSelector(); + customResourcePredicate = original.getEventFilter(); this.original = original; } @@ -65,6 +68,12 @@ public ControllerConfigurationOverrider withLabelSelector(String labelSelecto return this; } + public ControllerConfigurationOverrider withCustomResourcePredicate( + CustomResourceEventFilter customResourcePredicate) { + this.customResourcePredicate = customResourcePredicate; + return this; + } + public ControllerConfiguration build() { return new DefaultControllerConfiguration<>( original.getAssociatedControllerClassName(), @@ -75,6 +84,7 @@ public ControllerConfiguration build() { namespaces, retry, labelSelector, + customResourcePredicate, original.getCustomResourceClass(), original.getConfigurationService()); } 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 index 1b4a0a26f2..e5049fcfd0 100644 --- 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 @@ -4,6 +4,7 @@ import java.util.Set; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; public class DefaultControllerConfiguration> implements ControllerConfiguration { @@ -17,6 +18,7 @@ public class DefaultControllerConfiguration> private final boolean watchAllNamespaces; private final RetryConfiguration retryConfiguration; private final String labelSelector; + private final CustomResourceEventFilter customResourceEventFilter; private Class customResourceClass; private ConfigurationService service; @@ -29,6 +31,7 @@ public DefaultControllerConfiguration( Set namespaces, RetryConfiguration retryConfiguration, String labelSelector, + CustomResourceEventFilter customResourceEventFilter, Class customResourceClass, ConfigurationService service) { this.associatedControllerClassName = associatedControllerClassName; @@ -44,6 +47,7 @@ public DefaultControllerConfiguration( ? ControllerConfiguration.super.getRetryConfiguration() : retryConfiguration; this.labelSelector = labelSelector; + this.customResourceEventFilter = customResourceEventFilter; this.customResourceClass = customResourceClass == null ? ControllerConfiguration.super.getCustomResourceClass() : customResourceClass; @@ -52,7 +56,7 @@ public DefaultControllerConfiguration( /** * @deprecated use - * {@link #DefaultControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration, String, Class, ConfigurationService)} + * {@link #DefaultControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration)} * instead */ @Deprecated @@ -64,8 +68,18 @@ public DefaultControllerConfiguration( boolean generationAware, Set namespaces, RetryConfiguration retryConfiguration) { - this(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, - retryConfiguration, null, null, null); + this( + associatedControllerClassName, + name, + crdName, + finalizer, + generationAware, + namespaces, + retryConfiguration, + null, + null, + null, + null); } @Override @@ -131,4 +145,9 @@ public String getLabelSelector() { public Class getCustomResourceClass() { return customResourceClass; } + + @Override + public CustomResourceEventFilter getEventFilter() { + return customResourceEventFilter; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java index 75807acb84..ec66ec210d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java @@ -23,13 +23,13 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; @SuppressWarnings("rawtypes") -public class CustomResourceCache { +public class CustomResourceCache> { private static final Logger log = LoggerFactory.getLogger(CustomResourceCache.class); private static final Predicate passthrough = o -> true; private final ObjectMapper objectMapper; - private final ConcurrentMap resources; + private final ConcurrentMap resources; private final Lock lock = new ReentrantLock(); public CustomResourceCache() { @@ -44,11 +44,12 @@ public CustomResourceCache(ObjectMapper objectMapper, Metrics metrics) { resources = metrics.monitorSizeOf(new ConcurrentHashMap<>(), "cache"); } - public void cacheResource(CustomResource resource) { + @SuppressWarnings("unchecked") + public void cacheResource(T resource) { cacheResource(resource, passthrough); } - public void cacheResource(CustomResource resource, Predicate predicate) { + public void cacheResource(T resource, Predicate predicate) { try { lock.lock(); final var uid = getUID(resource); @@ -56,7 +57,8 @@ public void cacheResource(CustomResource resource, Predicate pre if (passthrough != predicate) { log.trace("Update cache after condition is true: {}", getName(resource)); } - resources.put(uid, resource); + // defensive copy + resources.put(getUID(resource), clone(resource)); } } finally { lock.unlock(); @@ -70,11 +72,11 @@ public void cacheResource(CustomResource resource, Predicate pre * @param uuid * @return */ - public Optional getLatestResource(String uuid) { + public Optional getLatestResource(String uuid) { return Optional.ofNullable(resources.get(uuid)).map(this::clone); } - public List getLatestResources(Predicate selector) { + public List getLatestResources(Predicate selector) { try { lock.lock(); return resources.values().stream() @@ -98,16 +100,17 @@ public Set getLatestResourcesUids(Predicate selector) { } } - private CustomResource clone(CustomResource customResource) { + @SuppressWarnings("unchecked") + private T clone(CustomResource customResource) { try { - return objectMapper.readValue( + return (T) objectMapper.readValue( objectMapper.writeValueAsString(customResource), customResource.getClass()); } catch (JsonProcessingException e) { throw new IllegalStateException(e); } } - public CustomResource cleanup(String customResourceUid) { + public T cleanup(String customResourceUid) { return resources.remove(customResourceUid); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java new file mode 100644 index 0000000000..bae73dc3c3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java @@ -0,0 +1,58 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; + +/** + * A functional interface to determine whether resource events should be processed by the SDK. This + * allows users to more finely tuned which events trigger a reconciliation than was previously + * possible (where the logic was limited to generation-based checking). + * + * @param the type of custom resources handled by this filter + */ +@FunctionalInterface +public interface CustomResourceEventFilter { + + /** + * Determines whether the change between the old version of the resource and the new one needs to + * be propagated to the controller or not. + * + * @param configuration the target controller's configuration + * @param oldResource the old version of the resource, null if no old resource available + * @param newResource the new version of the resource + * @return {@code true} if the change needs to be propagated to the controller, {@code false} + * otherwise + */ + boolean acceptChange(ControllerConfiguration configuration, T oldResource, T newResource); + + /** + * Combines this filter with the provided one with an AND logic, i.e. the resulting filter will + * only accept the change if both this and the other filter accept it, reject it otherwise. + * + * @param other the possibly {@code null} other filter to combine this one with + * @return a composite filter implementing the AND logic between this and the provided filter + */ + default CustomResourceEventFilter and(CustomResourceEventFilter other) { + return other == null ? this + : (ControllerConfiguration configuration, T oldResource, T newResource) -> { + boolean result = acceptChange(configuration, oldResource, newResource); + return result && other.acceptChange(configuration, oldResource, newResource); + }; + } + + /** + * Combines this filter with the provided one with an OR logic, i.e. the resulting filter will + * accept the change if any of this or the other filter accept it, rejecting it only if both + * reject it. + * + * @param other the possibly {@code null} other filter to combine this one with + * @return a composite filter implementing the OR logic between this and the provided filter + */ + default CustomResourceEventFilter or(CustomResourceEventFilter other) { + return other == null ? this + : (ControllerConfiguration configuration, T oldResource, T newResource) -> { + boolean result = acceptChange(configuration, oldResource, newResource); + return result || other.acceptChange(configuration, oldResource, newResource); + }; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java new file mode 100644 index 0000000000..306cde0888 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java @@ -0,0 +1,163 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import io.fabric8.kubernetes.client.CustomResource; + +/** + * Convenience implementations of, and utility methods for, {@link CustomResourceEventFilter}. + */ +public final class CustomResourceEventFilters { + + private static final CustomResourceEventFilter USE_FINALIZER = + (configuration, oldResource, newResource) -> { + if (configuration.useFinalizer()) { + final var finalizer = configuration.getFinalizer(); + boolean oldFinalizer = oldResource == null || oldResource.hasFinalizer(finalizer); + boolean newFinalizer = newResource.hasFinalizer(finalizer); + + return !newFinalizer || !oldFinalizer; + } else { + return false; + } + }; + + private static final CustomResourceEventFilter GENERATION_AWARE = + (configuration, oldResource, newResource) -> oldResource == null + || !configuration.isGenerationAware() + || oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); + + private static final CustomResourceEventFilter PASSTHROUGH = + (configuration, oldResource, newResource) -> true; + + private static final CustomResourceEventFilter NONE = + (configuration, oldResource, newResource) -> false; + + private static final CustomResourceEventFilter MARKED_FOR_DELETION = + (configuration, oldResource, newResource) -> newResource.isMarkedForDeletion(); + + private CustomResourceEventFilters() {} + + /** + * Retrieves a filter that accepts all events. + * + * @param the type of custom resource the filter should handle + * @return a filter that accepts all events + */ + @SuppressWarnings("unchecked") + public static CustomResourceEventFilter passthrough() { + return (CustomResourceEventFilter) PASSTHROUGH; + } + + /** + * Retrieves a filter that reject all events. + * + * @param the type of custom resource the filter should handle + * @return a filter that reject all events + */ + @SuppressWarnings("unchecked") + public static CustomResourceEventFilter none() { + return (CustomResourceEventFilter) NONE; + } + + /** + * Retrieves a filter that accepts all events if generation-aware processing is not activated but + * only changes that represent a generation increase otherwise. + * + * @param the type of custom resource the filter should handle + * @return a filter accepting changes based on generation information + */ + @SuppressWarnings("unchecked") + public static CustomResourceEventFilter generationAware() { + return (CustomResourceEventFilter) GENERATION_AWARE; + } + + /** + * Retrieves a filter that accepts changes if the target controller uses a finalizer and that + * finalizer hasn't already been applied, rejecting them otherwise. + * + * @param the type of custom resource the filter should handle + * @return a filter accepting changes based on whether the finalizer is needed and has been + * applied + */ + @SuppressWarnings("unchecked") + public static CustomResourceEventFilter finalizerNeededAndApplied() { + return (CustomResourceEventFilter) USE_FINALIZER; + } + + /** + * Retrieves a filter that accepts changes if the custom resource is marked for deletion. + * + * @param the type of custom resource the filter should handle + * @return a filter accepting changes based on whether the Custom Resource is marked for deletion. + */ + @SuppressWarnings("unchecked") + public static CustomResourceEventFilter markedForDeletion() { + return (CustomResourceEventFilter) MARKED_FOR_DELETION; + } + + /** + * Combines the provided, potentially {@code null} filters with an AND logic, i.e. the resulting + * filter will only accept the change if all filters accept it, reject it otherwise. + * + * Note that the evaluation of filters is lazy: the result is returned as soon as possible without + * evaluating all filters if possible. + * + * @param items the filters to combine + * @param the type of custom resources the filters are supposed to handle + * @return a combined filter implementing the AND logic combination of the provided filters + */ + @SafeVarargs + public static > CustomResourceEventFilter and( + CustomResourceEventFilter... items) { + if (items == null) { + return none(); + } + + return (configuration, oldResource, newResource) -> { + for (int i = 0; i < items.length; i++) { + if (items[i] == null) { + continue; + } + + if (!items[i].acceptChange(configuration, oldResource, newResource)) { + return false; + } + } + + return true; + }; + } + + /** + * Combines the provided, potentially {@code null} filters with an OR logic, i.e. the resulting + * filter will accept the change if any of the filters accepts it, rejecting it only if all reject + * it. + *

+ * Note that the evaluation of filters is lazy: the result is returned as soon as possible without + * evaluating all filters if possible. + * + * @param items the filters to combine + * @param the type of custom resources the filters are supposed to handle + * @return a combined filter implementing the OR logic combination of both provided filters + */ + @SafeVarargs + public static > CustomResourceEventFilter or( + CustomResourceEventFilter... items) { + if (items == null) { + return none(); + } + + return (configuration, oldResource, newResource) -> { + for (int i = 0; i < items.length; i++) { + if (items[i] == null) { + continue; + } + + if (items[i].acceptChange(configuration, oldResource, newResource)) { + return true; + } + } + + return false; + }; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index b610c405c9..c7a959061b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -3,8 +3,6 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,23 +25,25 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; -/** This is a special case since is not bound to a single custom resource */ +/** + * This is a special case since is not bound to a single custom resource + */ public class CustomResourceEventSource> extends AbstractEventSource implements Watcher { private static final Logger log = LoggerFactory.getLogger(CustomResourceEventSource.class); private final ConfiguredController controller; - private final Map lastGenerationProcessedSuccessfully = new ConcurrentHashMap<>(); private final List watches; - private final CustomResourceCache customResourceCache; + private final CustomResourceCache customResourceCache; public CustomResourceEventSource(ConfiguredController controller) { this.controller = controller; this.watches = new LinkedList<>(); - final var configurationService = controller.getConfiguration().getConfigurationService(); - this.customResourceCache = new CustomResourceCache(configurationService.getObjectMapper(), - configurationService.getMetrics()); + + this.customResourceCache = new CustomResourceCache<>( + controller.getConfiguration().getConfigurationService().getObjectMapper(), + controller.getConfiguration().getConfigurationService().getMetrics()); } @Override @@ -103,6 +103,9 @@ public void eventReceived(Watcher.Action action, T customResource) { log.debug( "Event received for action: {}, resource: {}", action.name(), getName(customResource)); + final String uuid = KubernetesResourceUtils.getUID(customResource); + final T oldResource = customResourceCache.getLatestResource(uuid).orElse(null); + // cache the latest version of the CR customResourceCache.cacheResource(customResource); @@ -115,9 +118,15 @@ public void eventReceived(Watcher.Action action, T customResource) { return; } - if (!skipBecauseOfGeneration(customResource)) { + final CustomResourceEventFilter filter = CustomResourceEventFilters.or( + CustomResourceEventFilters.finalizerNeededAndApplied(), + CustomResourceEventFilters.markedForDeletion(), + CustomResourceEventFilters.and( + controller.getConfiguration().getEventFilter(), + CustomResourceEventFilters.generationAware())); + + if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { eventHandler.handleEvent(new CustomResourceEvent(action, customResource, this)); - markLastGenerationProcessed(customResource); } else { log.debug( "Skipping event handling resource {} with version: {}", @@ -126,38 +135,6 @@ public void eventReceived(Watcher.Action action, T customResource) { } } - private void markLastGenerationProcessed(T resource) { - if (controller.getConfiguration().isGenerationAware() - && resource.hasFinalizer(controller.getConfiguration().getFinalizer())) { - lastGenerationProcessedSuccessfully.put( - KubernetesResourceUtils.getUID(resource), resource.getMetadata().getGeneration()); - } - } - - private boolean skipBecauseOfGeneration(T customResource) { - if (!controller.getConfiguration().isGenerationAware()) { - return false; - } - // if CR being deleted generation is naturally not changing, so we process all the events - if (customResource.isMarkedForDeletion()) { - return false; - } - - // only proceed if we haven't already seen this custom resource generation - Long lastGeneration = - lastGenerationProcessedSuccessfully.get(customResource.getMetadata().getUid()); - if (lastGeneration == null) { - return false; - } else { - return customResource.getMetadata().getGeneration() <= lastGeneration; - } - } - - @Override - public void eventSourceDeRegisteredForResource(String customResourceUid) { - lastGenerationProcessedSuccessfully.remove(customResourceUid); - } - @Override public void onClose(WatcherException e) { if (e == null) { @@ -181,7 +158,7 @@ public void onClose(WatcherException e) { } // todo: remove - public CustomResourceCache getCache() { + public CustomResourceCache getCache() { return customResourceCache; } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java new file mode 100644 index 0000000000..ab41b833ff --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java @@ -0,0 +1,159 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CustomResourceEventFilterTest { + public static final String FINALIZER = "finalizer"; + + private EventHandler eventHandler; + + @BeforeEach + public void before() { + this.eventHandler = mock(EventHandler.class); + } + + @Test + public void eventFilteredByCustomPredicate() { + var config = new TestControllerConfig( + FINALIZER, + false, + (configuration, oldResource, newResource) -> oldResource == null || !Objects.equals( + oldResource.getStatus().getConfigMapStatus(), + newResource.getStatus().getConfigMapStatus())); + + var controller = new TestConfiguredController(config); + var eventSource = new CustomResourceEventSource<>(controller); + eventSource.setEventHandler(eventHandler); + + TestCustomResource cr = TestUtils.testCustomResource(); + cr.getMetadata().setFinalizers(List.of(FINALIZER)); + cr.getMetadata().setGeneration(1L); + cr.getStatus().setConfigMapStatus("1"); + + eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + verify(eventHandler, times(1)).handleEvent(any()); + + cr.getMetadata().setGeneration(1L); + cr.getStatus().setConfigMapStatus("1"); + + eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + verify(eventHandler, times(1)).handleEvent(any()); + } + + @Test + public void eventFilteredByCustomPredicateAndGenerationAware() { + var config = new TestControllerConfig( + FINALIZER, + true, + (configuration, oldResource, newResource) -> oldResource == null || !Objects.equals( + oldResource.getStatus().getConfigMapStatus(), + newResource.getStatus().getConfigMapStatus())); + + var controller = new TestConfiguredController(config); + var eventSource = new CustomResourceEventSource<>(controller); + eventSource.setEventHandler(eventHandler); + + TestCustomResource cr = TestUtils.testCustomResource(); + cr.getMetadata().setFinalizers(List.of(FINALIZER)); + cr.getMetadata().setGeneration(1L); + cr.getStatus().setConfigMapStatus("1"); + + eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + verify(eventHandler, times(1)).handleEvent(any()); + + cr.getMetadata().setGeneration(1L); + cr.getStatus().setConfigMapStatus("2"); + + eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + verify(eventHandler, times(1)).handleEvent(any()); + } + + @Test + public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { + var config = new TestControllerConfig( + FINALIZER, + false, + (configuration, oldResource, newResource) -> { + return !Objects.equals( + oldResource.getStatus().getConfigMapStatus(), + newResource.getStatus().getConfigMapStatus()); + }); + + when(config.getConfigurationService().getObjectMapper()) + .thenReturn(ConfigurationService.OBJECT_MAPPER); + + var controller = new TestConfiguredController(config); + var eventSource = new CustomResourceEventSource<>(controller); + eventSource.setEventHandler(eventHandler); + + TestCustomResource cr = TestUtils.testCustomResource(); + cr.getMetadata().setGeneration(1L); + cr.getStatus().setConfigMapStatus("1"); + + eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + verify(eventHandler, times(1)).handleEvent(any()); + + cr.getMetadata().setGeneration(1L); + cr.getStatus().setConfigMapStatus("1"); + + eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + verify(eventHandler, times(2)).handleEvent(any()); + } + + private static class TestControllerConfig extends + DefaultControllerConfiguration { + + public TestControllerConfig(String finalizer, boolean generationAware, + CustomResourceEventFilter eventFilter) { + super( + null, + null, + null, + finalizer, + generationAware, + null, + null, + null, + eventFilter, + TestCustomResource.class, + mock(ConfigurationService.class)); + + when(getConfigurationService().getObjectMapper()) + .thenReturn(ConfigurationService.OBJECT_MAPPER); + } + } + + private static class TestConfiguredController extends ConfiguredController { + + public TestConfiguredController(ControllerConfiguration configuration) { + super(null, configuration, null); + } + + @Override + public MixedOperation, Resource> getCRClient() { + return mock(MixedOperation.class); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index c4b3533b5d..727caf3be1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -113,16 +113,27 @@ public MixedOperation { public TestConfiguration(boolean generationAware) { - super(null, null, null, FINALIZER, generationAware, null, null, null, + super( + null, + null, + null, + FINALIZER, + generationAware, + null, + null, + null, + null, TestCustomResource.class, mock(ConfigurationService.class)); when(getConfigurationService().getObjectMapper()) .thenReturn(ConfigurationService.OBJECT_MAPPER); - when(getConfigurationService().getMetrics()).thenReturn(Metrics.NOOP); + when(getConfigurationService().getMetrics()) + .thenReturn(Metrics.NOOP); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index 139f7bd0e9..b6094a0ffe 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -146,7 +146,7 @@ public static class MyConfiguration implements ControllerConfiguration { + + @Override + protected TestCustomResourceSpec initSpec() { + return new TestCustomResourceSpec(); + } + + @Override + protected TestCustomResourceStatus initStatus() { + return new TestCustomResourceStatus(); + } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 363bae0c55..04502d217f 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -10,6 +10,8 @@ import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters; public class AnnotationConfiguration implements ControllerConfiguration { @@ -70,5 +72,32 @@ public void setConfigurationService(ConfigurationService service) { public String getAssociatedControllerClassName() { return controller.getClass().getCanonicalName(); } + + @SuppressWarnings("unchecked") + @Override + public CustomResourceEventFilter getEventFilter() { + CustomResourceEventFilter answer = null; + + var filterTypes = annotation.map(Controller::eventFilters); + if (filterTypes.isPresent()) { + for (var filterType : filterTypes.get()) { + try { + CustomResourceEventFilter filter = filterType.getConstructor().newInstance(); + + if (answer == null) { + answer = filter; + } else { + answer = filter.and(filter); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + return answer != null + ? answer + : CustomResourceEventFilters.passthrough(); + } } From 8f446a7681e5c819199b4d976d34d35597d62952 Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 30 Sep 2021 14:44:57 +0200 Subject: [PATCH 0079/1608] Fixing Javadoc that blocks the snapshot release --- .../operator/api/UpdateControl.java | 3 ++ .../AbstractControllerConfiguration.java | 2 +- .../api/config/ControllerConfiguration.java | 9 +++--- .../DefaultControllerConfiguration.java | 28 ------------------- .../processing/CustomResourceCache.java | 4 +-- .../operator/processing/event/Event.java | 1 + .../processing/retry/RetryExecution.java | 2 +- 7 files changed, 12 insertions(+), 37 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java index 9dea38aea0..d9e14a6c5a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java @@ -34,6 +34,9 @@ public static UpdateControl updateStatusSubResourc /** * As a results of this there will be two call to K8S API. First the custom resource will be * updates then the status sub-resource. + * + * @param customResource - custom resource to use in both API calls + * @return UpdateControl instance */ public static UpdateControl updateCustomResourceAndStatus( T customResource) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java index a3ba836922..65b4a7ebc3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java @@ -19,7 +19,7 @@ public AbstractControllerConfiguration(String associatedControllerClassName, Str Set namespaces, RetryConfiguration retryConfiguration) { super(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, - retryConfiguration); + retryConfiguration, null, null, null, null); } public AbstractControllerConfiguration(String associatedControllerClassName, String name, 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 b9bcad0290..a1d097daba 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 @@ -103,12 +103,11 @@ default boolean useFinalizer() { /** * Allow controllers to filter events before they are provided to the - * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. - *

- * Note that the provided filter is combined with {@link #isGenerationAware()} to compute the - * final set of fiolters that should be applied; + * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. Note that the provided + * filter is combined with {@link #isGenerationAware()} to compute the final set of fiolters that + * should be applied; * - * @return + * @return filter */ default CustomResourceEventFilter getEventFilter() { return CustomResourceEventFilters.passthrough(); 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 index e5049fcfd0..61a8e44f8b 100644 --- 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 @@ -54,34 +54,6 @@ public DefaultControllerConfiguration( setConfigurationService(service); } - /** - * @deprecated use - * {@link #DefaultControllerConfiguration(String, String, String, String, boolean, Set, RetryConfiguration)} - * instead - */ - @Deprecated - public DefaultControllerConfiguration( - String associatedControllerClassName, - String name, - String crdName, - String finalizer, - boolean generationAware, - Set namespaces, - RetryConfiguration retryConfiguration) { - this( - associatedControllerClassName, - name, - crdName, - finalizer, - generationAware, - namespaces, - retryConfiguration, - null, - null, - null, - null); - } - @Override public String getName() { return name; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java index ec66ec210d..0e6d313f37 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java @@ -69,8 +69,8 @@ public void cacheResource(T resource, Predicate predicate) { * We clone the object so the one in the cache is not changed by the controller or dispatcher. * Therefore the cached object always represents the object coming from the API server. * - * @param uuid - * @return + * @param uuid identifier of resource + * @return resource if found in cache */ public Optional getLatestResource(String uuid) { return Optional.ofNullable(resources.get(uuid)).map(this::clone); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java index 095467fd39..5c8ca47eb0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java @@ -17,6 +17,7 @@ public interface Event { /** * The selector used to determine the {@link CustomResource} for which a reconcile loop should be * triggered. + * @return predicate used to match the target CustomResource */ Predicate getCustomResourcesSelector(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java index 717ceee07b..f20ff724ad 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java @@ -10,7 +10,7 @@ public interface RetryExecution extends RetryInfo { * Calculates the delay for the next execution. This method should return 0, when called first * time; * - * @return + * @return the time to wait until the next execution in milliseconds */ Optional nextDelay(); } From f9ed8809f306de7bba0d9eba38fbddf81f83e64f Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 30 Sep 2021 14:48:28 +0200 Subject: [PATCH 0080/1608] Formatting fix --- .../io/javaoperatorsdk/operator/processing/event/Event.java | 1 + .../operator/processing/retry/RetryExecution.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java index 5c8ca47eb0..f36ca6caa0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java @@ -17,6 +17,7 @@ public interface Event { /** * The selector used to determine the {@link CustomResource} for which a reconcile loop should be * triggered. + * * @return predicate used to match the target CustomResource */ Predicate getCustomResourcesSelector(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java index f20ff724ad..9326d225d6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java @@ -10,7 +10,7 @@ public interface RetryExecution extends RetryInfo { * Calculates the delay for the next execution. This method should return 0, when called first * time; * - * @return the time to wait until the next execution in milliseconds + * @return the time to wait until the next execution in millisecondsz */ Optional nextDelay(); } From 4d36922c90c49767a355c44006fc5586fe91a967 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 1 Oct 2021 10:50:15 +0200 Subject: [PATCH 0081/1608] putting back depracted method --- .../DefaultControllerConfiguration.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 index 61a8e44f8b..9775ce2151 100644 --- 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 @@ -54,6 +54,29 @@ public DefaultControllerConfiguration( setConfigurationService(service); } + @Deprecated + public DefaultControllerConfiguration( + String associatedControllerClassName, + String name, + String crdName, + String finalizer, + boolean generationAware, + Set namespaces, + RetryConfiguration retryConfiguration) { + this( + associatedControllerClassName, + name, + crdName, + finalizer, + generationAware, + namespaces, + retryConfiguration, + null, + null, + null, + null); + } + @Override public String getName() { return name; From 3c24f702819504b3ed03c562feaea2c0cbf87463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 4 Oct 2021 18:15:52 +0200 Subject: [PATCH 0082/1608] fix: EventSourceManager access fix (#580) --- .../operator/processing/ConfiguredController.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index dbac8b4d03..d34317fdb4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -27,7 +27,7 @@ public class ConfiguredController> implements Res private final ResourceController controller; private final ControllerConfiguration configuration; private final KubernetesClient k8sClient; - private EventSourceManager manager; + private EventSourceManager eventSourceManager; public ConfiguredController(ResourceController controller, ControllerConfiguration configuration, @@ -174,7 +174,7 @@ public void start() throws OperatorException { } try { - DefaultEventSourceManager eventSourceManager = new DefaultEventSourceManager<>(this); + eventSourceManager = new DefaultEventSourceManager<>(this); controller.init(eventSourceManager); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); @@ -217,11 +217,14 @@ private boolean failOnMissingCurrentNS() { return false; } + public EventSourceManager getEventSourceManager() { + return eventSourceManager; + } @Override public void close() throws IOException { - if (manager != null) { - manager.close(); + if (eventSourceManager != null) { + eventSourceManager.close(); } } } From e0da323203694ffb8259eb2a517138521d5ae716 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Mon, 4 Oct 2021 13:05:18 +0200 Subject: [PATCH 0083/1608] fix: DefaultEventHandler should not fire events if the handler is being closed #578 --- .../processing/DefaultEventHandler.java | 41 +++++++++++++++---- .../event/DefaultEventSourceManager.java | 7 ++++ .../processing/DefaultEventHandlerTest.java | 18 +++++++- 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 74d9e994de..b44994a018 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -51,6 +51,7 @@ public void failedEvent(String uid, Event event) {} private final ExecutorService executor; private final String controllerName; private final ReentrantLock lock = new ReentrantLock(); + private volatile boolean running; private DefaultEventSourceManager eventSourceManager; public DefaultEventHandler(ConfiguredController controller) { @@ -67,6 +68,7 @@ public DefaultEventHandler(ConfiguredController controller) { private DefaultEventHandler(ExecutorService executor, String relatedControllerName, EventDispatcher eventDispatcher, Retry retry) { + this.running = true; this.executor = executor == null ? new ScheduledThreadPoolExecutor( @@ -75,27 +77,28 @@ private DefaultEventHandler(ExecutorService executor, String relatedControllerNa this.controllerName = relatedControllerName; this.eventDispatcher = eventDispatcher; this.retry = retry; - eventBuffer = new EventBuffer(); - } - - public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { - this.eventSourceManager = eventSourceManager; + this.eventBuffer = new EventBuffer(); } public static void setEventMonitor(EventMonitor monitor) { DefaultEventHandler.monitor = monitor; } - public interface EventMonitor { - void processedEvent(String uid, Event event); - - void failedEvent(String uid, Event event); + public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { + this.eventSourceManager = eventSourceManager; } @Override public void handleEvent(Event event) { + try { lock.lock(); + + if (!this.running) { + log.debug("Skipping event: {} because the event handler is shutting down", event); + return; + } + log.debug("Received event: {}", event); final Predicate selector = event.getCustomResourcesSelector(); @@ -109,6 +112,16 @@ public void handleEvent(Event event) { } } + @Override + public void close() { + try { + lock.lock(); + this.running = false; + } finally { + lock.unlock(); + } + } + private void executeBufferedEvents(String customResourceUid) { boolean newEventForResourceId = eventBuffer.containsEvents(customResourceUid); boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); @@ -143,6 +156,10 @@ void eventProcessingFinished( ExecutionScope executionScope, PostExecutionControl postExecutionControl) { try { lock.lock(); + if (!running) { + return; + } + log.debug( "Event processing finished. Scope: {}, PostExecutionControl: {}", executionScope, @@ -279,6 +296,12 @@ private void unsetUnderExecution(String customResourceUid) { underProcessing.remove(customResourceUid); } + public interface EventMonitor { + void processedEvent(String uid, Event event); + + void failedEvent(String uid, Event event); + } + private class ControllerExecution implements Runnable { private final ExecutionScope executionScope; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 3aad5131f8..0ca4ffd289 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -54,6 +54,13 @@ public DefaultEventSourceManager(ConfiguredController controller) { public void close() { try { lock.lock(); + + try { + defaultEventHandler.close(); + } catch (Exception e) { + log.warn("Error closing event handler", e); + } + for (var entry : eventSources.entrySet()) { try { log.debug("Closing {} -> {}", entry.getKey(), entry.getValue()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 591c7fa1e0..c712a0d8be 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -23,7 +23,15 @@ import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class DefaultEventHandlerTest { @@ -224,6 +232,14 @@ public void scheduleTimedEventIfInstructedByPostExecutionControl() { .scheduleOnce(any(), eq(testDelay)); } + @Test + public void doNotFireEventsIfClosing() { + defaultEventHandler.close(); + defaultEventHandler.handleEvent(prepareCREvent()); + + verify(eventDispatcherMock, timeout(50).times(0)).handleExecution(any()); + } + private void waitMinimalTime() { try { Thread.sleep(50); From c92540e86fe46f89f1e2586c0693bbe8eeba025c Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 4 Oct 2021 11:12:39 +0200 Subject: [PATCH 0084/1608] feat: extract micrometer support into its own module Fixes #576. This allows getting rid of mandatory micrometer dependency if users are not interested in having metrics. --- micrometer-support/pom.xml | 31 ++++ .../micrometer/MicrometerMetrics.java | 66 ++++++++ operator-framework-core/pom.xml | 4 - .../io/javaoperatorsdk/operator/Metrics.java | 143 ++---------------- pom.xml | 1 + 5 files changed, 107 insertions(+), 138 deletions(-) create mode 100644 micrometer-support/pom.xml create mode 100644 micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml new file mode 100644 index 0000000000..f38349c232 --- /dev/null +++ b/micrometer-support/pom.xml @@ -0,0 +1,31 @@ + + + + java-operator-sdk + io.javaoperatorsdk + 1.9.8-SNAPSHOT + + 4.0.0 + + micrometer-support + Operator SDK - Micrometer Support + + + 11 + 11 + + + + + io.micrometer + micrometer-core + + + io.javaoperatorsdk + operator-framework-core + + + + \ No newline at end of file diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java new file mode 100644 index 0000000000..1555b10af7 --- /dev/null +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java @@ -0,0 +1,66 @@ +package io.javaoperatorsdk.operator.micrometer; + +import java.util.Collections; +import java.util.Map; + +import io.javaoperatorsdk.operator.Metrics; +import io.javaoperatorsdk.operator.Metrics.ControllerExecution; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +public class MicrometerMetrics implements Metrics { + + public static final String PREFIX = "operator.sdk."; + private final MeterRegistry registry; + + public MicrometerMetrics(MeterRegistry registry) { + this.registry = registry; + } + + public T timeControllerExecution(ControllerExecution execution) { + final var name = execution.controllerName(); + final var execName = PREFIX + "controllers.execution." + execution.name(); + final var timer = + Timer.builder(execName) + .tags("controller", name) + .publishPercentiles(0.3, 0.5, 0.95) + .publishPercentileHistogram() + .register(registry); + try { + final var result = timer.record(execution::execute); + final var successType = execution.successTypeName(result); + registry + .counter(execName + ".success", "controller", name, "type", successType) + .increment(); + return result; + } catch (Exception e) { + final var exception = e.getClass().getSimpleName(); + registry + .counter(execName + ".failure", "controller", name, "exception", exception) + .increment(); + throw e; + } + } + + public void incrementControllerRetriesNumber() { + registry + .counter( + PREFIX + "retry.on.exception", "retry", "retryCounter", "type", + "retryException") + .increment(); + + } + + public void incrementProcessedEventsNumber() { + registry + .counter( + PREFIX + "total.events.received", "events", "totalEvents", "type", + "eventsReceived") + .increment(); + + } + + public > T monitorSizeOf(T map, String name) { + return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); + } +} diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 33ba79521f..0ed9219c18 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -62,10 +62,6 @@ org.slf4j slf4j-api - - io.micrometer - micrometer-core - org.junit.jupiter diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java index 31269b5d41..79274b681d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java @@ -1,41 +1,12 @@ package io.javaoperatorsdk.operator; -import java.util.Collections; import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.function.ToDoubleFunction; -import java.util.function.ToLongFunction; -import io.micrometer.core.instrument.Clock; -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.DistributionSummary; -import io.micrometer.core.instrument.FunctionCounter; -import io.micrometer.core.instrument.FunctionTimer; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.Measurement; -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; -import io.micrometer.core.instrument.distribution.pause.PauseDetector; -import io.micrometer.core.instrument.noop.NoopCounter; -import io.micrometer.core.instrument.noop.NoopDistributionSummary; -import io.micrometer.core.instrument.noop.NoopFunctionCounter; -import io.micrometer.core.instrument.noop.NoopFunctionTimer; -import io.micrometer.core.instrument.noop.NoopGauge; -import io.micrometer.core.instrument.noop.NoopMeter; -import io.micrometer.core.instrument.noop.NoopTimer; +public interface Metrics { + Metrics NOOP = new Metrics() {}; -public class Metrics { - public static final Metrics NOOP = new Metrics(new NoopMeterRegistry(Clock.SYSTEM)); - public static final String PREFIX = "operator.sdk."; - private final MeterRegistry registry; - public Metrics(MeterRegistry registry) { - this.registry = registry; - } - - public interface ControllerExecution { + interface ControllerExecution { String name(); String controllerName(); @@ -45,111 +16,15 @@ public interface ControllerExecution { T execute(); } - public T timeControllerExecution(ControllerExecution execution) { - final var name = execution.controllerName(); - final var execName = PREFIX + "controllers.execution." + execution.name(); - final var timer = - Timer.builder(execName) - .tags("controller", name) - .publishPercentiles(0.3, 0.5, 0.95) - .publishPercentileHistogram() - .register(registry); - try { - final var result = timer.record(execution::execute); - final var successType = execution.successTypeName(result); - registry - .counter(execName + ".success", "controller", name, "type", successType) - .increment(); - return result; - } catch (Exception e) { - final var exception = e.getClass().getSimpleName(); - registry - .counter(execName + ".failure", "controller", name, "exception", exception) - .increment(); - throw e; - } - } - - public void incrementControllerRetriesNumber() { - registry - .counter( - PREFIX + "retry.on.exception", "retry", "retryCounter", "type", - "retryException") - .increment(); - + default T timeControllerExecution(ControllerExecution execution) { + return execution.execute(); } - public void incrementProcessedEventsNumber() { - registry - .counter( - PREFIX + "total.events.received", "events", "totalEvents", "type", - "eventsReceived") - .increment(); - - } - - public > T monitorSizeOf(T map, String name) { - return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); - } - - public static class NoopMeterRegistry extends MeterRegistry { - public NoopMeterRegistry(Clock clock) { - super(clock); - } - - @Override - protected Gauge newGauge(Meter.Id id, T t, ToDoubleFunction toDoubleFunction) { - return new NoopGauge(id); - } - - @Override - protected Counter newCounter(Meter.Id id) { - return new NoopCounter(id); - } - - @Override - protected Timer newTimer( - Meter.Id id, - DistributionStatisticConfig distributionStatisticConfig, - PauseDetector pauseDetector) { - return new NoopTimer(id); - } - - @Override - protected DistributionSummary newDistributionSummary( - Meter.Id id, DistributionStatisticConfig distributionStatisticConfig, double v) { - return new NoopDistributionSummary(id); - } - - @Override - protected Meter newMeter(Meter.Id id, Meter.Type type, Iterable iterable) { - return new NoopMeter(id); - } - - @Override - protected FunctionTimer newFunctionTimer( - Meter.Id id, - T t, - ToLongFunction toLongFunction, - ToDoubleFunction toDoubleFunction, - TimeUnit timeUnit) { - return new NoopFunctionTimer(id); - } - - @Override - protected FunctionCounter newFunctionCounter( - Meter.Id id, T t, ToDoubleFunction toDoubleFunction) { - return new NoopFunctionCounter(id); - } + default void incrementControllerRetriesNumber() {} - @Override - protected TimeUnit getBaseTimeUnit() { - return TimeUnit.SECONDS; - } + default void incrementProcessedEventsNumber() {} - @Override - protected DistributionStatisticConfig defaultHistogramConfig() { - return DistributionStatisticConfig.NONE; - } + default > T monitorSizeOf(T map, String name) { + return map; } } diff --git a/pom.xml b/pom.xml index baa171c9fd..cbc7448686 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,7 @@ operator-framework-junit5 operator-framework samples + micrometer-support From b54e610e9bf1a9ac4297897112adf0f2a605ee54 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 4 Oct 2021 16:39:11 +0200 Subject: [PATCH 0085/1608] refactor: do not rely on DefaultEventHandler so much for EventMonitor The goal is to not rely on DefaultEventHandler eventually. EventMonitor was kept on DefaultEventHandler for backwards compatibility reason but this should be moved to its own package along with the Metrics class for v2 --- .../micrometer/MicrometerMetrics.java | 19 +++++- .../io/javaoperatorsdk/operator/Metrics.java | 7 +++ .../io/javaoperatorsdk/operator/Operator.java | 14 ----- .../processing/DefaultEventHandler.java | 58 ++++++++++++++----- 4 files changed, 67 insertions(+), 31 deletions(-) diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java index 1555b10af7..7dd5515173 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java @@ -4,7 +4,8 @@ import java.util.Map; import io.javaoperatorsdk.operator.Metrics; -import io.javaoperatorsdk.operator.Metrics.ControllerExecution; +import io.javaoperatorsdk.operator.processing.DefaultEventHandler.EventMonitor; +import io.javaoperatorsdk.operator.processing.event.Event; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; @@ -12,6 +13,17 @@ public class MicrometerMetrics implements Metrics { public static final String PREFIX = "operator.sdk."; private final MeterRegistry registry; + private final EventMonitor monitor = new EventMonitor() { + @Override + public void processedEvent(String uid, Event event) { + incrementProcessedEventsNumber(); + } + + @Override + public void failedEvent(String uid, Event event) { + incrementControllerRetriesNumber(); + } + }; public MicrometerMetrics(MeterRegistry registry) { this.registry = registry; @@ -63,4 +75,9 @@ public void incrementProcessedEventsNumber() { public > T monitorSizeOf(T map, String name) { return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); } + + @Override + public EventMonitor getEventMonitor() { + return monitor; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java index 79274b681d..a3f3ddccb5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java @@ -2,6 +2,9 @@ import java.util.Map; +import io.javaoperatorsdk.operator.processing.DefaultEventHandler; +import io.javaoperatorsdk.operator.processing.DefaultEventHandler.EventMonitor; + public interface Metrics { Metrics NOOP = new Metrics() {}; @@ -27,4 +30,8 @@ default void incrementProcessedEventsNumber() {} default > T monitorSizeOf(T map, String name) { return map; } + + default DefaultEventHandler.EventMonitor getEventMonitor() { + return EventMonitor.NOOP; + } } 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 834c516c68..c61bcb04b3 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 @@ -18,9 +18,6 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.processing.ConfiguredController; -import io.javaoperatorsdk.operator.processing.DefaultEventHandler; -import io.javaoperatorsdk.operator.processing.DefaultEventHandler.EventMonitor; -import io.javaoperatorsdk.operator.processing.event.Event; @SuppressWarnings("rawtypes") public class Operator implements AutoCloseable { @@ -32,17 +29,6 @@ public class Operator implements AutoCloseable { public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) { this.k8sClient = k8sClient; this.configurationService = configurationService; - DefaultEventHandler.setEventMonitor(new EventMonitor() { - @Override - public void processedEvent(String uid, Event event) { - configurationService.getMetrics().incrementProcessedEventsNumber(); - } - - @Override - public void failedEvent(String uid, Event event) { - configurationService.getMetrics().incrementControllerRetriesNumber(); - } - }); } /** Adds a shutdown hook that automatically calls {@link #close()} when the app shuts down. */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index b44994a018..34201ae2df 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; @@ -35,13 +36,9 @@ public class DefaultEventHandler> implements EventHandler { private static final Logger log = LoggerFactory.getLogger(DefaultEventHandler.class); - private static EventMonitor monitor = new EventMonitor() { - @Override - public void processedEvent(String uid, Event event) {} - @Override - public void failedEvent(String uid, Event event) {} - }; + @Deprecated + private static EventMonitor monitor = EventMonitor.NOOP; private final EventBuffer eventBuffer; private final Set underProcessing = new HashSet<>(); @@ -51,6 +48,7 @@ public void failedEvent(String uid, Event event) {} private final ExecutorService executor; private final String controllerName; private final ReentrantLock lock = new ReentrantLock(); + private final EventMonitor eventMonitor; private volatile boolean running; private DefaultEventSourceManager eventSourceManager; @@ -58,16 +56,17 @@ public DefaultEventHandler(ConfiguredController controller) { this(ExecutorServiceManager.instance().executorService(), controller.getConfiguration().getName(), new EventDispatcher<>(controller), - GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration())); + GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), + controller.getConfiguration().getConfigurationService().getMetrics().getEventMonitor()); } DefaultEventHandler(EventDispatcher eventDispatcher, String relatedControllerName, Retry retry) { - this(null, relatedControllerName, eventDispatcher, retry); + this(null, relatedControllerName, eventDispatcher, retry, null); } private DefaultEventHandler(ExecutorService executor, String relatedControllerName, - EventDispatcher eventDispatcher, Retry retry) { + EventDispatcher eventDispatcher, Retry retry, EventMonitor monitor) { this.running = true; this.executor = executor == null @@ -78,14 +77,44 @@ private DefaultEventHandler(ExecutorService executor, String relatedControllerNa this.eventDispatcher = eventDispatcher; this.retry = retry; this.eventBuffer = new EventBuffer(); + this.eventMonitor = monitor != null ? monitor : EventMonitor.NOOP; + } + + public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { + this.eventSourceManager = eventSourceManager; } + /** + * @deprecated the EventMonitor to be used should now be retrieved from + * {@link Metrics#getEventMonitor()} + * @param monitor + */ + @Deprecated public static void setEventMonitor(EventMonitor monitor) { DefaultEventHandler.monitor = monitor; } - public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { - this.eventSourceManager = eventSourceManager; + /* + * TODO: promote this interface to top-level, probably create a `monitoring` package? + */ + public interface EventMonitor { + EventMonitor NOOP = new EventMonitor() { + @Override + public void processedEvent(String uid, Event event) {} + + @Override + public void failedEvent(String uid, Event event) {} + }; + + void processedEvent(String uid, Event event); + + void failedEvent(String uid, Event event); + } + + private EventMonitor monitor() { + // todo: remove us of static monitor, only here for backwards compatibility + return DefaultEventHandler.monitor != EventMonitor.NOOP ? DefaultEventHandler.monitor + : eventMonitor; } @Override @@ -102,6 +131,7 @@ public void handleEvent(Event event) { log.debug("Received event: {}", event); final Predicate selector = event.getCustomResourcesSelector(); + final var monitor = monitor(); for (String uid : eventSourceManager.getLatestResourceUids(selector)) { eventBuffer.addEvent(uid, event); monitor.processedEvent(uid, event); @@ -168,6 +198,7 @@ void eventProcessingFinished( if (retry != null && postExecutionControl.exceptionDuringExecution()) { handleRetryOnException(executionScope); + final var monitor = monitor(); executionScope.getEvents() .forEach(e -> monitor.failedEvent(executionScope.getCustomResourceUid(), e)); return; @@ -296,11 +327,6 @@ private void unsetUnderExecution(String customResourceUid) { underProcessing.remove(customResourceUid); } - public interface EventMonitor { - void processedEvent(String uid, Event event); - - void failedEvent(String uid, Event event); - } private class ControllerExecution implements Runnable { private final ExecutionScope executionScope; From abc689bb57a166383c6f1d15a19a9f51e90a2c87 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Oct 2021 09:17:52 +0000 Subject: [PATCH 0086/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- samples/common/pom.xml | 2 +- samples/pom.xml | 2 +- samples/pure-java/pom.xml | 2 +- samples/spring-boot-plain/pom.xml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index f38349c232..a12c83738b 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 0ed9219c18..e578810de5 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index e90ceede1e..6256cc7de3 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index b1f19d1b74..996c29bcae 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index cbc7448686..470cc27b92 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/samples/common/pom.xml b/samples/common/pom.xml index ca60b80c33..066f5007b3 100644 --- a/samples/common/pom.xml +++ b/samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT operator-framework-samples-common diff --git a/samples/pom.xml b/samples/pom.xml index e08a1f54d7..73528a4b20 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT java-operator-sdk-samples diff --git a/samples/pure-java/pom.xml b/samples/pure-java/pom.xml index a0af88e06b..c53735c45b 100644 --- a/samples/pure-java/pom.xml +++ b/samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT operator-framework-samples-pure-java diff --git a/samples/spring-boot-plain/pom.xml b/samples/spring-boot-plain/pom.xml index 5dc5bd2b6b..e361ca1cfc 100644 --- a/samples/spring-boot-plain/pom.xml +++ b/samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.8-SNAPSHOT + 1.9.9-SNAPSHOT operator-framework-samples-spring-boot-plain From 520e615553197c8ab546d1f5c52f0b352cff19b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 6 Oct 2021 14:59:05 +0200 Subject: [PATCH 0087/1608] feature: improvements on reschedule (#587) The the execution is rescheduled only if there are no buffered events. --- .../operator/processing/DefaultEventHandler.java | 10 +++++++--- .../processing/DefaultEventHandlerTest.java | 13 +++++++++++++ samples/common/crd/test_object.yaml | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 34201ae2df..27843a669a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -152,7 +152,7 @@ public void close() { } } - private void executeBufferedEvents(String customResourceUid) { + private boolean executeBufferedEvents(String customResourceUid) { boolean newEventForResourceId = eventBuffer.containsEvents(customResourceUid); boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); Optional latestCustomResource = @@ -167,6 +167,7 @@ private void executeBufferedEvents(String customResourceUid) { retryInfo(customResourceUid)); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ControllerExecution(executionScope)); + return true; } else { log.debug( "Skipping executing controller for resource id: {}. Events in queue: {}." @@ -175,6 +176,7 @@ private void executeBufferedEvents(String customResourceUid) { newEventForResourceId, controllerUnderExecution, latestCustomResource.isPresent()); + return false; } } @@ -211,8 +213,10 @@ void eventProcessingFinished( cleanupAfterDeletedEvent(executionScope.getCustomResourceUid()); } else { cacheUpdatedResourceIfChanged(executionScope, postExecutionControl); - reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getCustomResource()); - executeBufferedEvents(executionScope.getCustomResourceUid()); + var executed = executeBufferedEvents(executionScope.getCustomResourceUid()); + if (!executed) { + reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getCustomResource()); + } } } finally { lock.unlock(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index c712a0d8be..a03f4bd8a1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -232,6 +232,19 @@ public void scheduleTimedEventIfInstructedByPostExecutionControl() { .scheduleOnce(any(), eq(testDelay)); } + @Test + public void reScheduleOnlyIfNotExecutedBufferedEvents() { + var testDelay = 10000l; + when(eventDispatcherMock.handleExecution(any())) + .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); + + defaultEventHandler.handleEvent(prepareCREvent()); + defaultEventHandler.handleEvent(prepareCREvent()); + + verify(retryTimerEventSourceMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(0)) + .scheduleOnce(any(), eq(testDelay)); + } + @Test public void doNotFireEventsIfClosing() { defaultEventHandler.close(); diff --git a/samples/common/crd/test_object.yaml b/samples/common/crd/test_object.yaml index f8e23e387b..d897b550ef 100644 --- a/samples/common/crd/test_object.yaml +++ b/samples/common/crd/test_object.yaml @@ -4,4 +4,4 @@ metadata: name: custom-service1 spec: name: testservice1 - label: testlabel \ No newline at end of file + label: testlabel From b41282194f52cf0ac782317c00d2f72803be6193 Mon Sep 17 00:00:00 2001 From: Ioannis Canellos Date: Wed, 6 Oct 2021 14:22:24 +0300 Subject: [PATCH 0088/1608] refactor: Allow creating an Operator without a KubernetesClient instance. --- README.md | 3 +-- docs/documentation/use-samples.md | 3 +-- .../io/javaoperatorsdk/operator/Operator.java | 19 ++++++++++++------- .../sample/CustomServiceController.java | 5 +++++ .../sample/PureJavaApplicationRunner.java | 9 +++------ .../operator/sample/Config.java | 15 ++++----------- 6 files changed, 26 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 4e153f881a..2988e55408 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,7 @@ controller. public class Runner { public static void main(String[] args) { - Operator operator = new Operator(new DefaultKubernetesClient(), - DefaultConfigurationService.instance()); + Operator operator = new Operator(DefaultConfigurationService.instance()); operator.register(new WebServerController()); } } diff --git a/docs/documentation/use-samples.md b/docs/documentation/use-samples.md index 9e826daa24..a736aabe49 100644 --- a/docs/documentation/use-samples.md +++ b/docs/documentation/use-samples.md @@ -50,8 +50,7 @@ controller. public class Runner { public static void main(String[] args) { - Operator operator = new Operator(new DefaultKubernetesClient(), - DefaultConfigurationService.instance()); + Operator operator = new Operator(DefaultConfigurationService.instance()); operator.register(new WebServerController()); } } 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 c61bcb04b3..31bf1a80ec 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 @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Version; import io.javaoperatorsdk.operator.api.ResourceController; @@ -22,12 +23,16 @@ @SuppressWarnings("rawtypes") public class Operator implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(Operator.class); - private final KubernetesClient k8sClient; + private final KubernetesClient kubernetesClient; private final ConfigurationService configurationService; private final ControllerManager controllers = new ControllerManager(); - public Operator(KubernetesClient k8sClient, ConfigurationService configurationService) { - this.k8sClient = k8sClient; + public Operator(ConfigurationService configurationService) { + this(new DefaultKubernetesClient(), configurationService); + } + + public Operator(KubernetesClient kubernetesClient, ConfigurationService configurationService) { + this.kubernetesClient = kubernetesClient; this.configurationService = configurationService; } @@ -37,7 +42,7 @@ public void installShutdownHook() { } public KubernetesClient getKubernetesClient() { - return k8sClient; + return kubernetesClient; } public ConfigurationService getConfigurationService() { @@ -65,7 +70,7 @@ public void start() { log.info("Client version: {}", Version.clientVersion()); try { - final var k8sVersion = k8sClient.getVersion(); + final var k8sVersion = kubernetesClient.getVersion(); if (k8sVersion != null) { log.info("Server version: {}.{}", k8sVersion.getMajor(), k8sVersion.getMinor()); } @@ -93,7 +98,7 @@ public void close() { controllers.close(); ExecutorServiceManager.close(); - k8sClient.close(); + kubernetesClient.close(); } /** @@ -138,7 +143,7 @@ public void register( configuration = existing; } final var configuredController = - new ConfiguredController(controller, configuration, k8sClient); + new ConfiguredController(controller, configuration, kubernetesClient); controllers.add(configuredController); final var watchedNS = diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java index a4e8d66f45..aacff140c9 100644 --- a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java +++ b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.ServiceSpec; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.Controller; @@ -24,6 +25,10 @@ public class CustomServiceController implements ResourceController controllers) { - Operator operator = new Operator(client, DefaultConfigurationService.instance()); + public Operator operator(List controllers) { + Operator operator = new Operator(DefaultConfigurationService.instance()); controllers.forEach(operator::register); return operator; } From 2e8f219437e53b9ca58f146a24397dbf38f0bfb8 Mon Sep 17 00:00:00 2001 From: Ioannis Canellos Date: Wed, 6 Oct 2021 14:05:39 +0300 Subject: [PATCH 0089/1608] chore: renaming vars named k8sClient to kubernetsClient --- .../operator/processing/ConfiguredController.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index d34317fdb4..120a05bb65 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -26,15 +26,15 @@ public class ConfiguredController> implements Res Closeable { private final ResourceController controller; private final ControllerConfiguration configuration; - private final KubernetesClient k8sClient; + private final KubernetesClient kubernetesClient; private EventSourceManager eventSourceManager; public ConfiguredController(ResourceController controller, ControllerConfiguration configuration, - KubernetesClient k8sClient) { + KubernetesClient kubernetesClient) { this.controller = controller; this.configuration = configuration; - this.k8sClient = k8sClient; + this.kubernetesClient = kubernetesClient; } @Override @@ -140,11 +140,11 @@ public ControllerConfiguration getConfiguration() { } public KubernetesClient getClient() { - return k8sClient; + return kubernetesClient; } public MixedOperation, Resource> getCRClient() { - return k8sClient.resources(configuration.getCustomResourceClass()); + return kubernetesClient.resources(configuration.getCustomResourceClass()); } /** @@ -164,7 +164,8 @@ public void start() throws OperatorException { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config if (configuration.getConfigurationService().checkCRDAndValidateLocalModel()) { - crd = k8sClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get(); + crd = + kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get(); if (crd == null) { throwMissingCRDException(crdName, specVersion, controllerName); } From 9ebfff090bd0ba70553c1b87fea6574481e7c388 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Oct 2021 09:08:53 +0200 Subject: [PATCH 0090/1608] chore(deps): bump jandex-maven-plugin from 1.1.1 to 1.2.1 (#592) Bumps [jandex-maven-plugin](https://github.com/wildfly/jandex-maven-plugin) from 1.1.1 to 1.2.1. - [Release notes](https://github.com/wildfly/jandex-maven-plugin/releases) - [Commits](https://github.com/wildfly/jandex-maven-plugin/compare/1.1.1...1.2.1) --- updated-dependencies: - dependency-name: org.jboss.jandex:jandex-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 470cc27b92..d9d8da3a9b 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 2.8.2 2.5.2 5.0.0 - 1.1.1 + 1.2.1 2.16.0 1.0 1.6.2 From f1f3867c6dd7c270de5a7d6ce3c1a2d187cc79cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Oct 2021 09:09:08 +0200 Subject: [PATCH 0091/1608] chore(deps-dev): bump mockito-core from 3.12.4 to 4.0.0 (#591) Bumps [mockito-core](https://github.com/mockito/mockito) from 3.12.4 to 4.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v3.12.4...v4.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d9d8da3a9b..ac2324c699 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 5.8.0 1.7.32 2.14.1 - 3.12.4 + 4.0.0 3.12.0 1.0 0.19 From b99ff10323cdca0d218c9206eb0f54a4ed43057c Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 7 Oct 2021 13:55:03 +0200 Subject: [PATCH 0092/1608] feature: Build PR on v2 --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2f031c070e..6c33c1a710 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: true on: pull_request: - branches: [ master ] + branches: [ master, v2 ] workflow_dispatch: jobs: build: From 326f82f18b242f9f1f5ea1bc3921b09077c0e066 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 23 Sep 2021 23:17:06 +0200 Subject: [PATCH 0093/1608] chore(ci): use Java 17 --- .github/workflows/master-snapshot-release.yml | 2 +- .github/workflows/pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index 8c1d1d0049..305cf37279 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11, 16] + java: [ 11, 17 ] distribution: [temurin, adopt-openj9] kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6c33c1a710..77ca08961a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ 11 ] + java: [ 11, 17 ] distribution: [ temurin, adopt-openj9 ] kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: From 98e3def23afebeee11062735dc1c4269f8bfe0c8 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 24 Sep 2021 09:53:10 +0200 Subject: [PATCH 0094/1608] chore(ci): use only Temurin distribution --- .github/workflows/master-snapshot-release.yml | 2 +- .github/workflows/pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index 305cf37279..1fb5ac7e2e 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: java: [ 11, 17 ] - distribution: [temurin, adopt-openj9] + distribution: [ temurin ] kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 77ca08961a..a1ee4de399 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: java: [ 11, 17 ] - distribution: [ temurin, adopt-openj9 ] + distribution: [ temurin ] kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: - uses: actions/checkout@v2 From 746f0c1fae41454d6af613dd812351af30a5b4c6 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Mon, 11 Oct 2021 14:47:40 +0200 Subject: [PATCH 0095/1608] chore: add generics to PostExecutionControl to reduce IDEs noise (#594) --- .../operator/processing/PostExecutionControl.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java index 763e01c181..60d0f4bb34 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java @@ -21,12 +21,12 @@ private PostExecutionControl( this.runtimeException = runtimeException; } - public static PostExecutionControl onlyFinalizerAdded() { - return new PostExecutionControl(true, null, null); + public static > PostExecutionControl onlyFinalizerAdded() { + return new PostExecutionControl<>(true, null, null); } - public static PostExecutionControl defaultDispatch() { - return new PostExecutionControl(false, null, null); + public static > PostExecutionControl defaultDispatch() { + return new PostExecutionControl<>(false, null, null); } public static > PostExecutionControl customResourceUpdated( @@ -34,8 +34,9 @@ public static PostExecutionControl defaultDispatch() { return new PostExecutionControl<>(false, updatedCustomResource, null); } - public static PostExecutionControl exceptionDuringExecution(RuntimeException exception) { - return new PostExecutionControl(false, null, exception); + public static > PostExecutionControl exceptionDuringExecution( + RuntimeException exception) { + return new PostExecutionControl<>(false, null, exception); } public boolean isOnlyFinalizerHandled() { @@ -54,7 +55,7 @@ public boolean exceptionDuringExecution() { return runtimeException != null; } - public PostExecutionControl withReSchedule(long delay) { + public PostExecutionControl withReSchedule(long delay) { this.reScheduleDelay = delay; return this; } From bb6c00ea654a3c95d1e4b685b2851ac429735963 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Mon, 11 Oct 2021 14:48:19 +0200 Subject: [PATCH 0096/1608] chore: polish the junit5 extension (#593) --- .../operator/junit/OperatorExtension.java | 11 ++++++----- .../io/javaoperatorsdk/operator/ConcurrencyIT.java | 2 +- .../operator/ControllerExecutionIT.java | 4 ++-- .../operator/InformerEventSourceIT.java | 2 +- .../java/io/javaoperatorsdk/operator/RetryIT.java | 2 +- .../javaoperatorsdk/operator/SubResourceUpdateIT.java | 2 +- .../operator/UpdatingResAndSubResIT.java | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 743e2c7b72..b48aca918b 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -67,6 +67,11 @@ private OperatorExtension( this.waitForNamespaceDeletion = waitForNamespaceDeletion; } + /** + * Creates a {@link Builder} to set up an {@link OperatorExtension} instance. + * + * @return the builder. + */ public static Builder builder() { return new Builder(); } @@ -123,7 +128,7 @@ public NonNamespaceOperation T getNamedResource(Class type, String name) { + public T get(Class type, String name) { return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); } @@ -135,10 +140,6 @@ public T replace(Class type, T resource) { return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); } - public T get(Class type, String name) { - return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); - } - @SuppressWarnings("unchecked") protected void before(ExtensionContext context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index 6fa248a3d9..4332faf413 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -58,7 +58,7 @@ public void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedExcepti // update some resources for (int i = 0; i < NUMBER_OF_RESOURCES_UPDATED; i++) { TestCustomResource tcr = - operator.getNamedResource(TestCustomResource.class, + operator.get(TestCustomResource.class, TestUtils.TEST_CUSTOM_RESOURCE_PREFIX + i); tcr.getSpec().setValue(i + UPDATED_SUFFIX); operator.resources(TestCustomResource.class) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index 4855e61f49..cb3cdbb704 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -52,7 +52,7 @@ void awaitResourcesCreatedOrUpdated() { .untilAsserted( () -> { ConfigMap configMap = - operator.getNamedResource(ConfigMap.class, "test-config-map"); + operator.get(ConfigMap.class, "test-config-map"); assertThat(configMap).isNotNull(); assertThat(configMap.getData().get("test-key")).isEqualTo("test-value"); }); @@ -68,7 +68,7 @@ void awaitStatusUpdated(int timeout) { .untilAsserted( () -> { TestCustomResource cr = - operator.getNamedResource(TestCustomResource.class, + operator.get(TestCustomResource.class, TestUtils.TEST_CUSTOM_RESOURCE_NAME); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 66b9d73b98..1cb3fa7096 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -70,7 +70,7 @@ private InformerEventSourceTestCustomResource initialCustomResource() { private void waitForCRStatusValue(String value) { await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { var cr = - operator.getNamedResource(InformerEventSourceTestCustomResource.class, RESOURCE_NAME); + operator.get(InformerEventSourceTestCustomResource.class, RESOURCE_NAME); assertThat(cr.getStatus()).isNotNull(); assertThat(cr.getStatus().getConfigMapValue()).isEqualTo(value); }); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index a68ba0bc7f..e5fcde6934 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -52,7 +52,7 @@ public void retryFailedExecution() { .isEqualTo(RetryTestCustomResourceController.NUMBER_FAILED_EXECUTIONS + 1); RetryTestCustomResource finalResource = - operator.getNamedResource(RetryTestCustomResource.class, + operator.get(RetryTestCustomResource.class, resource.getMetadata().getName()); assertThat(finalResource.getStatus().getState()) .isEqualTo(RetryTestCustomResourceStatus.State.SUCCESS); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index c09982b639..1cee81c054 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -99,7 +99,7 @@ void awaitStatusUpdated(String name) { .untilAsserted( () -> { SubResourceTestCustomResource cr = - operator.getNamedResource(SubResourceTestCustomResource.class, name); + operator.get(SubResourceTestCustomResource.class, name); assertThat(cr.getMetadata().getFinalizers()).hasSize(1); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index 7d513ddd33..7d08ca6110 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -36,7 +36,7 @@ public void updatesSubResourceStatus() { DoubleUpdateTestCustomResource customResource = operator - .getNamedResource(DoubleUpdateTestCustomResource.class, + .get(DoubleUpdateTestCustomResource.class, resource.getMetadata().getName()); assertThat(TestUtils.getNumberOfExecutions(operator)) @@ -57,7 +57,7 @@ void awaitStatusUpdated(String name) { .untilAsserted( () -> { DoubleUpdateTestCustomResource cr = - operator.getNamedResource(DoubleUpdateTestCustomResource.class, name); + operator.get(DoubleUpdateTestCustomResource.class, name); assertThat(cr) .isNotNull(); From 0aa5b4959dfe303b191a1350379df5235409af04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 12 Oct 2021 14:22:25 +0200 Subject: [PATCH 0097/1608] Informer based CustomResourceEventSource and caching (#581) feat: This is a major change and backbone of v2. We change also how the Resources are addressed both internally and from event source with CustomResourceID. Additional improvements also added. * chore: renaming vars named k8sClient to kubernetsClient * chore(deps): bump jandex-maven-plugin from 1.1.1 to 1.2.1 (#592) Bumps [jandex-maven-plugin](https://github.com/wildfly/jandex-maven-plugin) from 1.1.1 to 1.2.1. - [Release notes](https://github.com/wildfly/jandex-maven-plugin/releases) - [Commits](https://github.com/wildfly/jandex-maven-plugin/compare/1.1.1...1.2.1) --- updated-dependencies: - dependency-name: org.jboss.jandex:jandex-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump mockito-core from 3.12.4 to 4.0.0 (#591) Bumps [mockito-core](https://github.com/mockito/mockito) from 3.12.4 to 4.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v3.12.4...v4.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: Build PR on v2 * chore(ci): use Java 17 * chore(ci): use only Temurin distribution * chore: add generics to PostExecutionControl to reduce IDEs noise (#594) * chore: polish the junit5 extension (#593) * feat: Use informers as CustomResourceEventSource backbone and cache Co-authored-by: Ioannis Canellos Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chris Laprun Co-authored-by: Luca Burgazzoli --- .github/workflows/master-snapshot-release.yml | 4 +- .github/workflows/pr.yml | 6 +- .gitignore | 2 + .../micrometer/MicrometerMetrics.java | 5 +- .../operator/ControllerUtils.java | 2 +- .../operator/EventListUtils.java | 4 +- .../operator/api/Controller.java | 8 +- .../AbstractControllerConfiguration.java | 35 ---- .../DefaultControllerConfiguration.java | 25 +-- .../processing/ConfiguredController.java | 13 +- .../processing/CustomResourceCache.java | 116 -------------- .../processing/DefaultEventHandler.java | 142 ++++++----------- .../operator/processing/EventBuffer.java | 21 ++- .../operator/processing/EventDispatcher.java | 1 - .../operator/processing/ExecutionScope.java | 5 +- .../processing/PostExecutionControl.java | 15 +- .../operator/processing/ResourceCache.java | 12 ++ .../processing/event/AbstractEvent.java | 21 --- .../processing/event/CustomResourceID.java | 50 ++++++ .../processing/event/DefaultEvent.java | 62 +------- .../event/DefaultEventSourceManager.java | 75 +++------ .../operator/processing/event/Event.java | 22 +-- .../processing/event/EventSource.java | 2 +- .../processing/event/EventSourceManager.java | 8 +- .../event/internal/CustomResourceEvent.java | 50 ++---- .../internal/CustomResourceEventFilters.java | 12 +- .../internal/CustomResourceEventSource.java | 149 ++++++++++-------- .../event/internal/InformerEvent.java | 31 ++-- .../event/internal/InformerEventSource.java | 21 +-- .../event/internal/LabelSelectorParser.java | 23 +++ .../processing/event/internal/Mappers.java | 33 ++-- .../event/internal/ResourceAction.java | 5 + .../processing/event/internal/TimerEvent.java | 5 +- .../event/internal/TimerEventSource.java | 25 ++- .../javaoperatorsdk/operator/TestUtils.java | 15 +- .../CustomResourceSelectorTest.java | 110 ------------- .../processing/DefaultEventHandlerTest.java | 84 +++++----- .../operator/processing/EventBufferTest.java | 20 +-- .../processing/EventDispatcherTest.java | 48 +++--- .../event/DefaultEventSourceManagerTest.java | 15 +- .../processing/event/EventListTest.java | 5 +- .../CustomResourceEventFilterTest.java | 26 +-- .../CustomResourceEventSourceTest.java | 39 +++-- .../internal/CustomResourceSelectorTest.java | 21 ++- .../internal/LabelSelectorParserTest.java | 29 ++++ .../event/internal/TimerEventSourceTest.java | 17 +- .../operator/junit/OperatorExtension.java | 11 +- .../runtime/AccumulativeMappingWriter.java | 2 +- .../runtime/AnnotationConfiguration.java | 40 +++-- .../config/runtime/ClassMappingProvider.java | 2 +- .../operator/ConcurrencyIT.java | 2 +- .../operator/ControllerExecutionIT.java | 4 +- .../operator/EventSourceIT.java | 6 +- .../operator/InformerEventSourceIT.java | 2 +- .../io/javaoperatorsdk/operator/RetryIT.java | 2 +- .../operator/SubResourceUpdateIT.java | 2 +- .../operator/UpdatingResAndSubResIT.java | 4 +- ...entSourceTestCustomResourceController.java | 2 +- pom.xml | 7 +- 59 files changed, 615 insertions(+), 910 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParser.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceAction.java delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParserTest.java diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index 8c1d1d0049..1fb5ac7e2e 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -15,8 +15,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11, 16] - distribution: [temurin, adopt-openj9] + java: [ 11, 17 ] + distribution: [ temurin ] kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2f031c070e..a1ee4de399 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,15 +8,15 @@ concurrency: cancel-in-progress: true on: pull_request: - branches: [ master ] + branches: [ master, v2 ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest strategy: matrix: - java: [ 11 ] - distribution: [ temurin, adopt-openj9 ] + java: [ 11, 17 ] + distribution: [ temurin ] kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index d82a982579..1f3ddc0c32 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ target/ # VSCode .factorypath + +.mvn/wrapper/maven-wrapper.jar diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java index 7dd5515173..3c8074f8f8 100644 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java @@ -5,6 +5,7 @@ import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.processing.DefaultEventHandler.EventMonitor; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; @@ -15,12 +16,12 @@ public class MicrometerMetrics implements Metrics { private final MeterRegistry registry; private final EventMonitor monitor = new EventMonitor() { @Override - public void processedEvent(String uid, Event event) { + public void processedEvent(CustomResourceID uid, Event event) { incrementProcessedEventsNumber(); } @Override - public void failedEvent(String uid, Event event) { + public void failedEvent(CustomResourceID uid, Event event) { incrementControllerRetriesNumber(); } }; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java index 0097cb128a..023b333758 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java @@ -18,7 +18,7 @@ public static String getNameFor(Class controllerCl final var annotation = controllerClass.getAnnotation(Controller.class); if (annotation != null) { final var name = annotation.name(); - if (!Controller.NULL.equals(name)) { + if (!Controller.EMPTY_STRING.equals(name)) { return name; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java index 852b630af0..0fe18bdae0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java @@ -2,9 +2,9 @@ import java.util.List; -import io.fabric8.kubernetes.client.Watcher; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; public class EventListUtils { @@ -13,7 +13,7 @@ public static boolean containsCustomResourceDeletedEvent(List events) { .anyMatch( e -> { if (e instanceof CustomResourceEvent) { - return ((CustomResourceEvent) e).getAction() == Watcher.Action.DELETED; + return ((CustomResourceEvent) e).getAction() == ResourceAction.DELETED; } else { return false; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java index ebf9c65d42..bf03b03309 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java @@ -11,11 +11,11 @@ @Target({ElementType.TYPE}) public @interface Controller { - String NULL = ""; + String EMPTY_STRING = ""; String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; String NO_FINALIZER = "JOSDK_NO_FINALIZER"; - String name() default NULL; + String name() default EMPTY_STRING; /** * Optional finalizer name, if it is not provided, one will be automatically generated. If the @@ -24,7 +24,7 @@ * * @return the finalizer name */ - String finalizerName() default NULL; + String finalizerName() default EMPTY_STRING; /** * If true, will dispatch new event to the controller if generation increased since the last @@ -50,7 +50,7 @@ * * @return the label selector */ - String labelSelector() default NULL; + String labelSelector() default EMPTY_STRING; /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java deleted file mode 100644 index 65b4a7ebc3..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractControllerConfiguration.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import java.util.Set; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; - -/** - * @deprecated use {@link DefaultControllerConfiguration} instead - * @param - */ -@Deprecated -public class AbstractControllerConfiguration> - extends DefaultControllerConfiguration { - - @Deprecated - public AbstractControllerConfiguration(String associatedControllerClassName, String name, - String crdName, String finalizer, boolean generationAware, - Set namespaces, - RetryConfiguration retryConfiguration) { - super(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, - retryConfiguration, null, null, null, null); - } - - public AbstractControllerConfiguration(String associatedControllerClassName, String name, - String crdName, String finalizer, boolean generationAware, - Set namespaces, - RetryConfiguration retryConfiguration, String labelSelector, - CustomResourceEventFilter customResourcePredicate, - Class customResourceClass, - ConfigurationService service) { - super(associatedControllerClassName, name, crdName, finalizer, generationAware, namespaces, - retryConfiguration, labelSelector, customResourcePredicate, customResourceClass, service); - } -} 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 index 9775ce2151..672704f3c4 100644 --- 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 @@ -19,7 +19,7 @@ public class DefaultControllerConfiguration> private final RetryConfiguration retryConfiguration; private final String labelSelector; private final CustomResourceEventFilter customResourceEventFilter; - private Class customResourceClass; + private final Class customResourceClass; private ConfigurationService service; public DefaultControllerConfiguration( @@ -54,29 +54,6 @@ public DefaultControllerConfiguration( setConfigurationService(service); } - @Deprecated - public DefaultControllerConfiguration( - String associatedControllerClassName, - String name, - String crdName, - String finalizer, - boolean generationAware, - Set namespaces, - RetryConfiguration retryConfiguration) { - this( - associatedControllerClassName, - name, - crdName, - finalizer, - generationAware, - namespaces, - retryConfiguration, - null, - null, - null, - null); - } - @Override public String getName() { return name; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index d34317fdb4..120a05bb65 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -26,15 +26,15 @@ public class ConfiguredController> implements Res Closeable { private final ResourceController controller; private final ControllerConfiguration configuration; - private final KubernetesClient k8sClient; + private final KubernetesClient kubernetesClient; private EventSourceManager eventSourceManager; public ConfiguredController(ResourceController controller, ControllerConfiguration configuration, - KubernetesClient k8sClient) { + KubernetesClient kubernetesClient) { this.controller = controller; this.configuration = configuration; - this.k8sClient = k8sClient; + this.kubernetesClient = kubernetesClient; } @Override @@ -140,11 +140,11 @@ public ControllerConfiguration getConfiguration() { } public KubernetesClient getClient() { - return k8sClient; + return kubernetesClient; } public MixedOperation, Resource> getCRClient() { - return k8sClient.resources(configuration.getCustomResourceClass()); + return kubernetesClient.resources(configuration.getCustomResourceClass()); } /** @@ -164,7 +164,8 @@ public void start() throws OperatorException { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config if (configuration.getConfigurationService().checkCRDAndValidateLocalModel()) { - crd = k8sClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get(); + crd = + kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get(); if (crd == null) { throwMissingCRDException(crdName, specVersion, controllerName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java deleted file mode 100644 index 0e6d313f37..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/CustomResourceCache.java +++ /dev/null @@ -1,116 +0,0 @@ -package io.javaoperatorsdk.operator.processing; - -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.Metrics; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; - -@SuppressWarnings("rawtypes") -public class CustomResourceCache> { - - private static final Logger log = LoggerFactory.getLogger(CustomResourceCache.class); - private static final Predicate passthrough = o -> true; - - private final ObjectMapper objectMapper; - private final ConcurrentMap resources; - private final Lock lock = new ReentrantLock(); - - public CustomResourceCache() { - this(new ObjectMapper(), Metrics.NOOP); - } - - public CustomResourceCache(ObjectMapper objectMapper, Metrics metrics) { - this.objectMapper = objectMapper; - if (metrics == null) { - metrics = Metrics.NOOP; - } - resources = metrics.monitorSizeOf(new ConcurrentHashMap<>(), "cache"); - } - - @SuppressWarnings("unchecked") - public void cacheResource(T resource) { - cacheResource(resource, passthrough); - } - - public void cacheResource(T resource, Predicate predicate) { - try { - lock.lock(); - final var uid = getUID(resource); - if (predicate.test(resources.get(uid))) { - if (passthrough != predicate) { - log.trace("Update cache after condition is true: {}", getName(resource)); - } - // defensive copy - resources.put(getUID(resource), clone(resource)); - } - } finally { - lock.unlock(); - } - } - - /** - * We clone the object so the one in the cache is not changed by the controller or dispatcher. - * Therefore the cached object always represents the object coming from the API server. - * - * @param uuid identifier of resource - * @return resource if found in cache - */ - public Optional getLatestResource(String uuid) { - return Optional.ofNullable(resources.get(uuid)).map(this::clone); - } - - public List getLatestResources(Predicate selector) { - try { - lock.lock(); - return resources.values().stream() - .filter(selector) - .map(this::clone) - .collect(Collectors.toList()); - } finally { - lock.unlock(); - } - } - - public Set getLatestResourcesUids(Predicate selector) { - try { - lock.lock(); - return resources.values().stream() - .filter(selector) - .map(r -> r.getMetadata().getUid()) - .collect(Collectors.toSet()); - } finally { - lock.unlock(); - } - } - - @SuppressWarnings("unchecked") - private T clone(CustomResource customResource) { - try { - return (T) objectMapper.readValue( - objectMapper.writeValueAsString(customResource), customResource.getClass()); - } catch (JsonProcessingException e) { - throw new IllegalStateException(e); - } - } - - public T cleanup(String customResourceUid) { - return resources.remove(customResourceUid); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 27843a669a..5d440193df 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -8,16 +8,15 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -27,7 +26,6 @@ import static io.javaoperatorsdk.operator.EventListUtils.containsCustomResourceDeletedEvent; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; /** * Event handler that makes sure that events are processed in a "single threaded" way per resource @@ -41,31 +39,36 @@ public class DefaultEventHandler> implements Even private static EventMonitor monitor = EventMonitor.NOOP; private final EventBuffer eventBuffer; - private final Set underProcessing = new HashSet<>(); + private final Set underProcessing = new HashSet<>(); private final EventDispatcher eventDispatcher; private final Retry retry; - private final Map retryState = new HashMap<>(); + private final Map retryState = new HashMap<>(); private final ExecutorService executor; private final String controllerName; private final ReentrantLock lock = new ReentrantLock(); private final EventMonitor eventMonitor; private volatile boolean running; + private final ResourceCache resourceCache; private DefaultEventSourceManager eventSourceManager; - public DefaultEventHandler(ConfiguredController controller) { - this(ExecutorServiceManager.instance().executorService(), + public DefaultEventHandler(ConfiguredController controller, ResourceCache resourceCache) { + this( + resourceCache, + ExecutorServiceManager.instance().executorService(), controller.getConfiguration().getName(), new EventDispatcher<>(controller), GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), controller.getConfiguration().getConfigurationService().getMetrics().getEventMonitor()); } - DefaultEventHandler(EventDispatcher eventDispatcher, String relatedControllerName, + DefaultEventHandler(EventDispatcher eventDispatcher, ResourceCache resourceCache, + String relatedControllerName, Retry retry) { - this(null, relatedControllerName, eventDispatcher, retry, null); + this(resourceCache, null, relatedControllerName, eventDispatcher, retry, null); } - private DefaultEventHandler(ExecutorService executor, String relatedControllerName, + private DefaultEventHandler(ResourceCache resourceCache, ExecutorService executor, + String relatedControllerName, EventDispatcher eventDispatcher, Retry retry, EventMonitor monitor) { this.running = true; this.executor = @@ -76,7 +79,8 @@ private DefaultEventHandler(ExecutorService executor, String relatedControllerNa this.controllerName = relatedControllerName; this.eventDispatcher = eventDispatcher; this.retry = retry; - this.eventBuffer = new EventBuffer(); + eventBuffer = new EventBuffer(); + this.resourceCache = resourceCache; this.eventMonitor = monitor != null ? monitor : EventMonitor.NOOP; } @@ -84,31 +88,21 @@ public void setEventSourceManager(DefaultEventSourceManager eventSourceManage this.eventSourceManager = eventSourceManager; } - /** - * @deprecated the EventMonitor to be used should now be retrieved from - * {@link Metrics#getEventMonitor()} - * @param monitor - */ - @Deprecated - public static void setEventMonitor(EventMonitor monitor) { - DefaultEventHandler.monitor = monitor; - } - /* * TODO: promote this interface to top-level, probably create a `monitoring` package? */ public interface EventMonitor { EventMonitor NOOP = new EventMonitor() { @Override - public void processedEvent(String uid, Event event) {} + public void processedEvent(CustomResourceID uid, Event event) {} @Override - public void failedEvent(String uid, Event event) {} + public void failedEvent(CustomResourceID uid, Event event) {} }; - void processedEvent(String uid, Event event); + void processedEvent(CustomResourceID uid, Event event); - void failedEvent(String uid, Event event); + void failedEvent(CustomResourceID uid, Event event); } private EventMonitor monitor() { @@ -119,24 +113,17 @@ private EventMonitor monitor() { @Override public void handleEvent(Event event) { - try { lock.lock(); - + log.debug("Received event: {}", event); if (!this.running) { log.debug("Skipping event: {} because the event handler is shutting down", event); return; } - - log.debug("Received event: {}", event); - - final Predicate selector = event.getCustomResourcesSelector(); final var monitor = monitor(); - for (String uid : eventSourceManager.getLatestResourceUids(selector)) { - eventBuffer.addEvent(uid, event); - monitor.processedEvent(uid, event); - executeBufferedEvents(uid); - } + eventBuffer.addEvent(event.getRelatedCustomResourceID(), event); + monitor.processedEvent(event.getRelatedCustomResourceID(), event); + executeBufferedEvents(event.getRelatedCustomResourceID()); } finally { lock.unlock(); } @@ -152,11 +139,11 @@ public void close() { } } - private boolean executeBufferedEvents(String customResourceUid) { + private boolean executeBufferedEvents(CustomResourceID customResourceUid) { boolean newEventForResourceId = eventBuffer.containsEvents(customResourceUid); boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); - Optional latestCustomResource = - eventSourceManager.getLatestResource(customResourceUid); + Optional latestCustomResource = + resourceCache.getCustomResource(customResourceUid); if (!controllerUnderExecution && newEventForResourceId && latestCustomResource.isPresent()) { setUnderExecutionProcessing(customResourceUid); @@ -176,11 +163,14 @@ private boolean executeBufferedEvents(String customResourceUid) { newEventForResourceId, controllerUnderExecution, latestCustomResource.isPresent()); + if (latestCustomResource.isEmpty()) { + log.warn("no custom resource found in cache for CustomResourceID: {}", customResourceUid); + } return false; } } - private RetryInfo retryInfo(String customResourceUid) { + private RetryInfo retryInfo(CustomResourceID customResourceUid) { return retryState.get(customResourceUid); } @@ -196,13 +186,13 @@ void eventProcessingFinished( "Event processing finished. Scope: {}, PostExecutionControl: {}", executionScope, postExecutionControl); - unsetUnderExecution(executionScope.getCustomResourceUid()); + unsetUnderExecution(executionScope.getCustomResourceID()); if (retry != null && postExecutionControl.exceptionDuringExecution()) { handleRetryOnException(executionScope); final var monitor = monitor(); executionScope.getEvents() - .forEach(e -> monitor.failedEvent(executionScope.getCustomResourceUid(), e)); + .forEach(e -> monitor.failedEvent(executionScope.getCustomResourceID(), e)); return; } @@ -210,10 +200,9 @@ void eventProcessingFinished( markSuccessfulExecutionRegardingRetry(executionScope); } if (containsCustomResourceDeletedEvent(executionScope.getEvents())) { - cleanupAfterDeletedEvent(executionScope.getCustomResourceUid()); + cleanupAfterDeletedEvent(executionScope.getCustomResourceID()); } else { - cacheUpdatedResourceIfChanged(executionScope, postExecutionControl); - var executed = executeBufferedEvents(executionScope.getCustomResourceUid()); + var executed = executeBufferedEvents(executionScope.getCustomResourceID()); if (!executed) { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getCustomResource()); } @@ -237,12 +226,14 @@ private void reScheduleExecutionIfInstructed(PostExecutionControl postExecuti */ private void handleRetryOnException(ExecutionScope executionScope) { RetryExecution execution = getOrInitRetryExecution(executionScope); - boolean newEventsExists = eventBuffer.newEventsExists(executionScope.getCustomResourceUid()); - eventBuffer.putBackEvents(executionScope.getCustomResourceUid(), executionScope.getEvents()); + var customResourceID = executionScope.getCustomResourceID(); + boolean newEventsExists = eventBuffer + .newEventsExists(customResourceID); + eventBuffer.putBackEvents(customResourceID, executionScope.getEvents()); if (newEventsExists) { - log.debug("New events exists for for resource id: {}", executionScope.getCustomResourceUid()); - executeBufferedEvents(executionScope.getCustomResourceUid()); + log.debug("New events exists for for resource id: {}", customResourceID); + executeBufferedEvents(customResourceID); return; } Optional nextDelay = execution.nextDelay(); @@ -252,7 +243,7 @@ private void handleRetryOnException(ExecutionScope executionScope) { log.debug( "Scheduling timer event for retry with delay:{} for resource: {}", delay, - executionScope.getCustomResourceUid()); + customResourceID); eventSourceManager .getRetryTimerEventSource() .scheduleOnce(executionScope.getCustomResource(), delay); @@ -264,70 +255,35 @@ private void markSuccessfulExecutionRegardingRetry(ExecutionScope executionSc log.debug( "Marking successful execution for resource: {}", getName(executionScope.getCustomResource())); - retryState.remove(executionScope.getCustomResourceUid()); + retryState.remove(executionScope.getCustomResourceID()); eventSourceManager .getRetryTimerEventSource() - .cancelOnceSchedule(executionScope.getCustomResourceUid()); + .cancelOnceSchedule(executionScope.getCustomResourceID()); } private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) { - RetryExecution retryExecution = retryState.get(executionScope.getCustomResourceUid()); + RetryExecution retryExecution = retryState.get(executionScope.getCustomResourceID()); if (retryExecution == null) { retryExecution = retry.initExecution(); - retryState.put(executionScope.getCustomResourceUid(), retryExecution); + retryState.put(executionScope.getCustomResourceID(), retryExecution); } return retryExecution; } - /** - * Here we try to cache the latest resource after an update. The goal is to solve a concurrency - * issue we've seen: If an execution is finished, where we updated a custom resource, but there - * are other events already buffered for next execution, we might not get the newest custom - * resource from CustomResource event source in time. Thus we execute the next batch of events but - * with a non up to date CR. Here we cache the latest CustomResource from the update execution so - * we make sure its already used in the up-coming execution. - * - *

- * Note that this is an improvement, not a bug fix. This situation can happen naturally, we just - * make the execution more efficient, and avoid questions about conflicts. - * - *

- * Note that without the conditional locking in the cache, there is a very minor chance that we - * would override an additional change coming from a different client. - */ - private void cacheUpdatedResourceIfChanged( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { - if (postExecutionControl.customResourceUpdatedDuringExecution()) { - R originalCustomResource = executionScope.getCustomResource(); - R customResourceAfterExecution = postExecutionControl.getUpdatedCustomResource().get(); - String originalResourceVersion = getVersion(originalCustomResource); - - log.debug( - "Trying to update resource cache from update response for resource: {} new version: {} old version: {}", - getName(originalCustomResource), - getVersion(customResourceAfterExecution), - getVersion(originalCustomResource)); - eventSourceManager.cacheResource( - customResourceAfterExecution, - customResource -> getVersion(customResource).equals(originalResourceVersion) - && !originalResourceVersion.equals(getVersion(customResourceAfterExecution))); - } - } - - private void cleanupAfterDeletedEvent(String customResourceUid) { + private void cleanupAfterDeletedEvent(CustomResourceID customResourceUid) { eventSourceManager.cleanup(customResourceUid); eventBuffer.cleanup(customResourceUid); } - private boolean isControllerUnderExecution(String customResourceUid) { + private boolean isControllerUnderExecution(CustomResourceID customResourceUid) { return underProcessing.contains(customResourceUid); } - private void setUnderExecutionProcessing(String customResourceUid) { + private void setUnderExecutionProcessing(CustomResourceID customResourceUid) { underProcessing.add(customResourceUid); } - private void unsetUnderExecution(String customResourceUid) { + private void unsetUnderExecution(CustomResourceID customResourceUid) { underProcessing.remove(customResourceUid); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java index c9d248acf2..b9c565a001 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java @@ -2,45 +2,44 @@ import java.util.*; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; class EventBuffer { - private final Map> events = new HashMap<>(); + private final Map> events = new HashMap<>(); - /** @deprecated use {@link #addEvent(String, Event)} */ - @Deprecated public void addEvent(Event event) { - addEvent(event.getRelatedCustomResourceUid(), event); + addEvent(event.getRelatedCustomResourceID(), event); } - public void addEvent(String uid, Event event) { + public void addEvent(CustomResourceID uid, Event event) { Objects.requireNonNull(uid, "uid"); Objects.requireNonNull(event, "event"); - List crEvents = events.computeIfAbsent(uid, (id) -> new LinkedList<>()); + List crEvents = events.computeIfAbsent(uid, (customResourceID) -> new LinkedList<>()); crEvents.add(event); } - public boolean newEventsExists(String resourceId) { + public boolean newEventsExists(CustomResourceID resourceId) { return events.get(resourceId) != null && !events.get(resourceId).isEmpty(); } - public void putBackEvents(String resourceUid, List oldEvents) { + public void putBackEvents(CustomResourceID resourceUid, List oldEvents) { List crEvents = events.computeIfAbsent(resourceUid, (id) -> new LinkedList<>()); crEvents.addAll(0, oldEvents); } - public boolean containsEvents(String customResourceId) { + public boolean containsEvents(CustomResourceID customResourceId) { return events.get(customResourceId) != null; } - public List getAndRemoveEventsForExecution(String resourceUid) { + public List getAndRemoveEventsForExecution(CustomResourceID resourceUid) { List crEvents = events.remove(resourceUid); return crEvents == null ? Collections.emptyList() : crEvents; } - public void cleanup(String resourceUid) { + public void cleanup(CustomResourceID resourceUid) { events.remove(resourceUid); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 85fcbefb9d..92347d5460 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -168,7 +168,6 @@ private PostExecutionControl handleDelete(R resource, Context context) { if (deleteControl == DeleteControl.DEFAULT_DELETE && resource.hasFinalizer(configuration().getFinalizer())) { R customResource = removeFinalizer(resource); - // todo: should we patch the resource to remove the finalizer instead of updating it return PostExecutionControl.customResourceUpdated(customResource); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java index 53917cc094..4f5f0ca7f7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java @@ -4,6 +4,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.RetryInfo; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; public class ExecutionScope> { @@ -27,8 +28,8 @@ public R getCustomResource() { return customResource; } - public String getCustomResourceUid() { - return customResource.getMetadata().getUid(); + public CustomResourceID getCustomResourceID() { + return CustomResourceID.fromResource(customResource); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java index 763e01c181..60d0f4bb34 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java @@ -21,12 +21,12 @@ private PostExecutionControl( this.runtimeException = runtimeException; } - public static PostExecutionControl onlyFinalizerAdded() { - return new PostExecutionControl(true, null, null); + public static > PostExecutionControl onlyFinalizerAdded() { + return new PostExecutionControl<>(true, null, null); } - public static PostExecutionControl defaultDispatch() { - return new PostExecutionControl(false, null, null); + public static > PostExecutionControl defaultDispatch() { + return new PostExecutionControl<>(false, null, null); } public static > PostExecutionControl customResourceUpdated( @@ -34,8 +34,9 @@ public static PostExecutionControl defaultDispatch() { return new PostExecutionControl<>(false, updatedCustomResource, null); } - public static PostExecutionControl exceptionDuringExecution(RuntimeException exception) { - return new PostExecutionControl(false, null, exception); + public static > PostExecutionControl exceptionDuringExecution( + RuntimeException exception) { + return new PostExecutionControl<>(false, null, exception); } public boolean isOnlyFinalizerHandled() { @@ -54,7 +55,7 @@ public boolean exceptionDuringExecution() { return runtimeException != null; } - public PostExecutionControl withReSchedule(long delay) { + public PostExecutionControl withReSchedule(long delay) { this.reScheduleDelay = delay; return this; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java new file mode 100644 index 0000000000..127926c724 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.processing; + +import java.util.Optional; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; + +public interface ResourceCache> { + + Optional getCustomResource(CustomResourceID resourceID); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java deleted file mode 100644 index 3429301fe1..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event; - -import java.util.function.Predicate; - -import io.fabric8.kubernetes.client.CustomResource; - -/** - * @deprecated use {@link DefaultEvent} instead - */ -@Deprecated -public class AbstractEvent extends DefaultEvent { - - public AbstractEvent(String relatedCustomResourceUid, EventSource eventSource) { - super(relatedCustomResourceUid, eventSource); - } - - public AbstractEvent( - Predicate customResourcesSelector, EventSource eventSource) { - super(customResourcesSelector, eventSource); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java new file mode 100644 index 0000000000..d405e48a5a --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.processing.event; + +import java.util.Objects; +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class CustomResourceID { + + public static CustomResourceID fromResource(HasMetadata resource) { + return new CustomResourceID(resource.getMetadata().getName(), + resource.getMetadata().getNamespace()); + } + + private final String name; + private final String namespace; + + public CustomResourceID(String name, String namespace) { + this.name = name; + this.namespace = namespace; + } + + public CustomResourceID(String name) { + this(name, null); + } + + public String getName() { + return name; + } + + public Optional getNamespace() { + return Optional.ofNullable(namespace); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + CustomResourceID that = (CustomResourceID) o; + return Objects.equals(name, that.name) && Objects.equals(namespace, + that.namespace); + } + + @Override + public int hashCode() { + return Objects.hash(name, namespace); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java index b966c06609..c445f2bf27 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java @@ -1,70 +1,24 @@ package io.javaoperatorsdk.operator.processing.event; -import java.util.Objects; -import java.util.function.Predicate; - -import io.fabric8.kubernetes.client.CustomResource; - @SuppressWarnings("rawtypes") public class DefaultEvent implements Event { - private final Predicate customResourcesSelector; - private final EventSource eventSource; - - public DefaultEvent(String relatedCustomResourceUid, EventSource eventSource) { - this.customResourcesSelector = new UIDMatchingPredicate(relatedCustomResourceUid); - this.eventSource = eventSource; - } + private final CustomResourceID relatedCustomResource; - public DefaultEvent(Predicate customResourcesSelector, EventSource eventSource) { - this.customResourcesSelector = customResourcesSelector; - this.eventSource = eventSource; - } - @Override - public String getRelatedCustomResourceUid() { - if (customResourcesSelector instanceof UIDMatchingPredicate) { - UIDMatchingPredicate resourcesSelector = (UIDMatchingPredicate) customResourcesSelector; - return resourcesSelector.uid; - } else { - return null; - } + public DefaultEvent(CustomResourceID targetCustomResource) { + this.relatedCustomResource = targetCustomResource; } - public Predicate getCustomResourcesSelector() { - return customResourcesSelector; - } @Override - public EventSource getEventSource() { - return eventSource; + public CustomResourceID getRelatedCustomResourceID() { + return relatedCustomResource; } @Override public String toString() { - return "{ class=" - + this.getClass().getName() - + ", customResourcesSelector=" - + customResourcesSelector - + ", eventSource=" - + eventSource - + " }"; - } - - public static class UIDMatchingPredicate implements Predicate { - private final String uid; - - public UIDMatchingPredicate(String uid) { - this.uid = uid; - } - - @Override - public boolean test(CustomResource customResource) { - return Objects.equals(uid, customResource.getMetadata().getUid()); - } - - @Override - public String toString() { - return "UIDMatchingPredicate{uid='" + uid + "'}"; - } + return "DefaultEvent{" + + "relatedCustomResource=" + relatedCustomResource + + '}'; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 0ca4ffd289..8142544c01 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -2,14 +2,11 @@ import java.io.IOException; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,7 +15,6 @@ import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.ConfiguredController; -import io.javaoperatorsdk.operator.processing.CustomResourceCache; import io.javaoperatorsdk.operator.processing.DefaultEventHandler; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; @@ -27,27 +23,31 @@ public class DefaultEventSourceManager> implements EventSourceManager { public static final String RETRY_TIMER_EVENT_SOURCE_NAME = "retry-timer-event-source"; - private static final String CUSTOM_RESOURCE_EVENT_SOURCE_NAME = "custom-resource-event-source"; + public static final String CUSTOM_RESOURCE_EVENT_SOURCE_NAME = "custom-resource-event-source"; private static final Logger log = LoggerFactory.getLogger(DefaultEventSourceManager.class); private final ReentrantLock lock = new ReentrantLock(); private final Map eventSources = new ConcurrentHashMap<>(); - private final DefaultEventHandler defaultEventHandler; + private DefaultEventHandler defaultEventHandler; private TimerEventSource retryTimerEventSource; - DefaultEventSourceManager(DefaultEventHandler defaultEventHandler, boolean supportRetry) { - this.defaultEventHandler = defaultEventHandler; - defaultEventHandler.setEventSourceManager(this); - if (supportRetry) { - this.retryTimerEventSource = new TimerEventSource<>(); - registerEventSource(RETRY_TIMER_EVENT_SOURCE_NAME, retryTimerEventSource); - } + DefaultEventSourceManager(DefaultEventHandler defaultEventHandler) { + init(defaultEventHandler); } public DefaultEventSourceManager(ConfiguredController controller) { - this(new DefaultEventHandler<>(controller), true); - registerEventSource(CUSTOM_RESOURCE_EVENT_SOURCE_NAME, - new CustomResourceEventSource<>(controller)); + CustomResourceEventSource customResourceEventSource = + new CustomResourceEventSource<>(controller); + init(new DefaultEventHandler<>(controller, customResourceEventSource)); + registerEventSource(CUSTOM_RESOURCE_EVENT_SOURCE_NAME, customResourceEventSource); + } + + private void init(DefaultEventHandler defaultEventHandler) { + this.defaultEventHandler = defaultEventHandler; + defaultEventHandler.setEventSourceManager(this); + + this.retryTimerEventSource = new TimerEventSource<>(); + registerEventSource(RETRY_TIMER_EVENT_SOURCE_NAME, retryTimerEventSource); } @Override @@ -122,7 +122,7 @@ public Optional deRegisterEventSource(String name) { @Override public Optional deRegisterCustomResourceFromEventSource( - String eventSourceName, String customResourceUid) { + String eventSourceName, CustomResourceID customResourceUid) { try { lock.lock(); EventSource eventSource = this.eventSources.get(eventSourceName); @@ -150,42 +150,15 @@ public Map getRegisteredEventSources() { return Collections.unmodifiableMap(eventSources); } - public void cleanup(String customResourceUid) { + @Override + public CustomResourceEventSource getCustomResourceEventSource() { + return (CustomResourceEventSource) getRegisteredEventSources() + .get(CUSTOM_RESOURCE_EVENT_SOURCE_NAME); + } + + public void cleanup(CustomResourceID customResourceUid) { getRegisteredEventSources() .keySet() .forEach(k -> deRegisterCustomResourceFromEventSource(k, customResourceUid)); - eventSources.remove(customResourceUid); - CustomResourceCache cache = getCache(); - if (cache != null) { - cache.cleanup(customResourceUid); - } - } - - // todo: remove - public CustomResourceCache getCache() { - final var source = - (CustomResourceEventSource) getRegisteredEventSources() - .get(CUSTOM_RESOURCE_EVENT_SOURCE_NAME); - return source.getCache(); - } - - // todo: remove - public Optional getLatestResource(String customResourceUid) { - return getCache().getLatestResource(customResourceUid); - } - - // todo: remove - public List getLatestResources(Predicate selector) { - return getCache().getLatestResources(selector); - } - - // todo: remove - public Set getLatestResourceUids(Predicate selector) { - return getCache().getLatestResourcesUids(selector); - } - - // todo: remove - public void cacheResource(CustomResource resource, Predicate predicate) { - getCache().cacheResource(resource, predicate); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java index f36ca6caa0..b0d51a37c0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java @@ -1,27 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; -import java.util.function.Predicate; - -import io.fabric8.kubernetes.client.CustomResource; - public interface Event { - /** - * @return the UID of the the {@link CustomResource} for which a reconcile loop should be - * triggered. - * @deprecated use {@link #getCustomResourcesSelector()} - */ - @Deprecated - String getRelatedCustomResourceUid(); - - /** - * The selector used to determine the {@link CustomResource} for which a reconcile loop should be - * triggered. - * - * @return predicate used to match the target CustomResource - */ - Predicate getCustomResourcesSelector(); + CustomResourceID getRelatedCustomResourceID(); - /** @return the {@link EventSource} that has generated the event. */ - EventSource getEventSource(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java index 149e0acc1c..1cc05f632c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java @@ -20,5 +20,5 @@ default void close() throws IOException {} void setEventHandler(EventHandler eventHandler); - default void eventSourceDeRegisteredForResource(String customResourceUid) {} + default void eventSourceDeRegisteredForResource(CustomResourceID customResourceUid) {} } 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 b59491042b..86638c2786 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 @@ -5,9 +5,11 @@ import java.util.Map; import java.util.Optional; +import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; -public interface EventSourceManager extends Closeable { +public interface EventSourceManager> extends Closeable { /** * Add the {@link EventSource} identified by the given name to the event manager. @@ -32,10 +34,12 @@ void registerEventSource(String name, EventSource eventSource) Optional deRegisterEventSource(String name); Optional deRegisterCustomResourceFromEventSource( - String name, String customResourceUid); + String name, CustomResourceID customResourceUid); Map getRegisteredEventSources(); + CustomResourceEventSource getCustomResourceEventSource(); + @Override default void close() throws IOException {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java index 007ce49edc..15a67108db 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java @@ -1,54 +1,36 @@ package io.javaoperatorsdk.operator.processing.event.internal; import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.Watcher; -import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; - public class CustomResourceEvent extends DefaultEvent { - private final Watcher.Action action; + private final ResourceAction action; private final CustomResource customResource; - public CustomResourceEvent( - Watcher.Action action, - CustomResource resource, - CustomResourceEventSource customResourceEventSource) { - super(KubernetesResourceUtils.getUID(resource), customResourceEventSource); - this.action = action; - this.customResource = resource; - } - - public Watcher.Action getAction() { - return action; - } - public String resourceUid() { - return getCustomResource().getMetadata().getUid(); + public CustomResourceEvent(ResourceAction action, + CustomResource resource) { + super(CustomResourceID.fromResource(resource)); + this.customResource = resource; + this.action = action; } @Override public String toString() { - return "CustomResourceEvent{" - + "action=" - + action - + ", resource=[ name=" - + getName(getCustomResource()) - + ", kind=" - + getCustomResource().getKind() - + ", apiVersion=" - + getCustomResource().getApiVersion() - + " ,resourceVersion=" - + getCustomResource().getMetadata().getResourceVersion() - + ", markedForDeletion: " - + (getCustomResource().getMetadata().getDeletionTimestamp() != null - && !getCustomResource().getMetadata().getDeletionTimestamp().isEmpty()) - + " ]}"; + return "CustomResourceEvent{" + + "action=" + action + + ", customResource=" + customResource + + '}'; } public CustomResource getCustomResource() { return customResource; } + + public ResourceAction getAction() { + return action; + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java index 306cde0888..2f3bc72327 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java @@ -113,12 +113,12 @@ public static CustomResourceEventFilter markedForD } return (configuration, oldResource, newResource) -> { - for (int i = 0; i < items.length; i++) { - if (items[i] == null) { + for (CustomResourceEventFilter item : items) { + if (item == null) { continue; } - if (!items[i].acceptChange(configuration, oldResource, newResource)) { + if (!item.acceptChange(configuration, oldResource, newResource)) { return false; } } @@ -147,12 +147,12 @@ public static CustomResourceEventFilter markedForD } return (configuration, oldResource, newResource) -> { - for (int i = 0; i < items.length; i++) { - if (items[i] == null) { + for (CustomResourceEventFilter item : items) { + if (item == null) { continue; } - if (items[i].acceptChange(configuration, oldResource, newResource)) { + if (item.acceptChange(configuration, oldResource, newResource)) { return true; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index c7a959061b..9964e7b450 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -1,49 +1,51 @@ package io.javaoperatorsdk.operator.processing.event.internal; import java.io.IOException; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.ListOptions; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.Watch; -import io.fabric8.kubernetes.client.Watcher; -import io.fabric8.kubernetes.client.WatcherException; -import io.fabric8.kubernetes.client.utils.Utils; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; -import io.javaoperatorsdk.operator.processing.CustomResourceCache; -import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; +import io.javaoperatorsdk.operator.processing.ResourceCache; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; +import static io.javaoperatorsdk.operator.processing.event.internal.LabelSelectorParser.parseSimpleLabelSelector; /** * This is a special case since is not bound to a single custom resource */ public class CustomResourceEventSource> extends AbstractEventSource - implements Watcher { + implements ResourceEventHandler, ResourceCache { + + public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; private static final Logger log = LoggerFactory.getLogger(CustomResourceEventSource.class); private final ConfiguredController controller; - private final List watches; - private final CustomResourceCache customResourceCache; + private final Map> sharedIndexInformers = + new ConcurrentHashMap<>(); + private final ObjectMapper cloningObjectMapper; public CustomResourceEventSource(ConfiguredController controller) { this.controller = controller; - this.watches = new LinkedList<>(); - - this.customResourceCache = new CustomResourceCache<>( - controller.getConfiguration().getConfigurationService().getObjectMapper(), - controller.getConfiguration().getConfigurationService().getMetrics()); + this.cloningObjectMapper = + controller.getConfiguration().getConfigurationService().getObjectMapper(); } @Override @@ -52,25 +54,25 @@ public void start() { final var targetNamespaces = configuration.getEffectiveNamespaces(); final var client = controller.getCRClient(); final var labelSelector = configuration.getLabelSelector(); - var options = new ListOptions(); - if (Utils.isNotNullOrEmpty(labelSelector)) { - options.setLabelSelector(labelSelector); - } try { if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { - var w = client.inAnyNamespace().watch(options, this); - watches.add(w); - log.debug("Registered {} -> {} for any namespace", controller, w); + var informer = client.inAnyNamespace() + .withLabels(parseSimpleLabelSelector(labelSelector)).inform(this); + sharedIndexInformers.put(ANY_NAMESPACE_MAP_KEY, informer); + log.debug("Registered {} -> {} for any namespace", controller, informer); } else { targetNamespaces.forEach( ns -> { - var w = client.inNamespace(ns).watch(options, this); - watches.add(w); - log.debug("Registered {} -> {} for namespace: {}", controller, w, ns); + var informer = client.inNamespace(ns) + .withLabels(parseSimpleLabelSelector(labelSelector)).inform(this); + sharedIndexInformers.put(ns, informer); + log.debug("Registered {} -> {} for namespace: {}", controller, informer, + ns); }); } } catch (Exception e) { + // todo double check this if still applies for informers if (e instanceof KubernetesClientException) { KubernetesClientException ke = (KubernetesClientException) e; if (404 == ke.getCode()) { @@ -88,35 +90,19 @@ public void start() { @Override public void close() throws IOException { eventHandler.close(); - for (Watch watch : this.watches) { + for (SharedIndexInformer informer : sharedIndexInformers.values()) { try { - log.info("Closing watch {} -> {}", controller, watch); - watch.close(); + log.info("Closing informer {} -> {}", controller, informer); + informer.close(); } catch (Exception e) { - log.warn("Error closing watcher {} -> {}", controller, watch, e); + log.warn("Error closing informer {} -> {}", controller, informer, e); } } } - @Override - public void eventReceived(Watcher.Action action, T customResource) { + public void eventReceived(ResourceAction action, T customResource, T oldResource) { log.debug( - "Event received for action: {}, resource: {}", action.name(), getName(customResource)); - - final String uuid = KubernetesResourceUtils.getUID(customResource); - final T oldResource = customResourceCache.getLatestResource(uuid).orElse(null); - - // cache the latest version of the CR - customResourceCache.cacheResource(customResource); - - if (action == Action.ERROR) { - log.debug( - "Skipping {} event for custom resource uid: {}, version: {}", - action, - getUID(customResource), - getVersion(customResource)); - return; - } + "Event received for resource: {}", getName(customResource)); final CustomResourceEventFilter filter = CustomResourceEventFilters.or( CustomResourceEventFilters.finalizerNeededAndApplied(), @@ -126,7 +112,7 @@ public void eventReceived(Watcher.Action action, T customResource) { CustomResourceEventFilters.generationAware())); if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { - eventHandler.handleEvent(new CustomResourceEvent(action, customResource, this)); + eventHandler.handleEvent(new CustomResourceEvent(action, clone(customResource))); } else { log.debug( "Skipping event handling resource {} with version: {}", @@ -136,29 +122,52 @@ public void eventReceived(Watcher.Action action, T customResource) { } @Override - public void onClose(WatcherException e) { - if (e == null) { - return; - } - if (e.isHttpGone()) { - log.warn("Received error for watch, will try to reconnect.", e); - try { - close(); - start(); - } catch (Throwable ex) { - log.error("Unexpected error happened with watch reconnect. Will exit.", e); - System.exit(1); - } + public void onAdd(T resource) { + eventReceived(ResourceAction.ADDED, resource, null); + } + + @Override + public void onUpdate(T oldCustomResource, T newCustomResource) { + eventReceived(ResourceAction.UPDATED, newCustomResource, oldCustomResource); + } + + @Override + public void onDelete(T resource, boolean b) { + eventReceived(ResourceAction.DELETED, resource, null); + } + + @Override + public Optional getCustomResource(CustomResourceID resourceID) { + var sharedIndexInformer = + sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); + var resource = sharedIndexInformer.getStore() + .getByKey(Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), + resourceID.getName())); + if (resource == null) { + return Optional.empty(); } else { - // Note that this should not happen normally, since fabric8 client handles reconnect. - // In case it tries to reconnect this method is not called. - log.error("Unexpected error happened with watch. Will exit.", e); - System.exit(1); + return Optional.of(clone(resource)); } } - // todo: remove - public CustomResourceCache getCache() { - return customResourceCache; + /** + * @return shared informers by namespace. If custom resource is not namespace scoped use + * CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY + */ + public Map> getInformers() { + return Collections.unmodifiableMap(sharedIndexInformers); + } + + public SharedIndexInformer getInformer(String namespace) { + return getInformers().get(Objects.requireNonNullElse(namespace, ANY_NAMESPACE_MAP_KEY)); + } + + private T clone(T customResource) { + try { + return (T) cloningObjectMapper.readValue( + cloningObjectMapper.writeValueAsString(customResource), customResource.getClass()); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java index ecfcece338..81c78d31b4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java @@ -1,30 +1,20 @@ package io.javaoperatorsdk.operator.processing.event.internal; import java.util.Optional; -import java.util.function.Predicate; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; -import io.javaoperatorsdk.operator.processing.event.EventSource; -public class InformerEvent extends DefaultEvent { +public class InformerEvent extends DefaultEvent { - private Action action; - private T resource; - private T oldResource; + private final ResourceAction action; + private final T resource; + private final T oldResource; - public InformerEvent(String relatedCustomResourceUid, EventSource eventSource, Action action, - T resource, - T oldResource) { - this(new UIDMatchingPredicate(relatedCustomResourceUid), eventSource, action, resource, - oldResource); - - } - - public InformerEvent(Predicate customResourcesSelector, EventSource eventSource, - Action action, + public InformerEvent(ResourceAction action, T resource, T oldResource) { - super(customResourcesSelector, eventSource); + super(CustomResourceID.fromResource(resource)); this.action = action; this.resource = resource; this.oldResource = oldResource; @@ -38,11 +28,8 @@ public Optional getOldResource() { return Optional.ofNullable(oldResource); } - public Action getAction() { + public ResourceAction getAction() { return action; } - public enum Action { - ADD, UPDATE, DELETE - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 37e1add66e..34e409fa17 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -12,33 +12,34 @@ import io.fabric8.kubernetes.client.informers.cache.Cache; import io.fabric8.kubernetes.client.informers.cache.Store; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; public class InformerEventSource extends AbstractEventSource { private final SharedInformer sharedInformer; - private final Function> resourceToUIDs; + private final Function> resourceToUIDs; private final Function associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToUIDs) { + Function> resourceToUIDs) { this(sharedInformer, resourceToUIDs, null, true); } public InformerEventSource(KubernetesClient client, Class type, - Function> resourceToUIDs) { + Function> resourceToUIDs) { this(client, type, resourceToUIDs, false); } InformerEventSource(KubernetesClient client, Class type, - Function> resourceToUIDs, + Function> resourceToUIDs, boolean skipUpdateEventPropagationIfNoChange) { this(client.informers().sharedIndexInformerFor(type, 0), resourceToUIDs, null, skipUpdateEventPropagationIfNoChange); } public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToUIDs, + Function> resourceToUIDs, Function associatedWith, boolean skipUpdateEventPropagationIfNoChange) { this.sharedInformer = sharedInformer; @@ -54,7 +55,7 @@ public InformerEventSource(SharedInformer sharedInformer, sharedInformer.addEventHandler(new ResourceEventHandler<>() { @Override public void onAdd(T t) { - propagateEvent(InformerEvent.Action.ADD, t, null); + propagateEvent(ResourceAction.ADDED, t, null); } @Override @@ -64,23 +65,23 @@ public void onUpdate(T oldObject, T newObject) { .equals(newObject.getMetadata().getResourceVersion())) { return; } - propagateEvent(InformerEvent.Action.UPDATE, newObject, oldObject); + propagateEvent(ResourceAction.UPDATED, newObject, oldObject); } @Override public void onDelete(T t, boolean b) { - propagateEvent(InformerEvent.Action.DELETE, t, null); + propagateEvent(ResourceAction.DELETED, t, null); } }); } - private void propagateEvent(InformerEvent.Action action, T object, T oldObject) { + private void propagateEvent(ResourceAction action, T object, T oldObject) { var uids = resourceToUIDs.apply(object); if (uids.isEmpty()) { return; } uids.forEach(uid -> { - InformerEvent event = new InformerEvent(uid, this, action, object, oldObject); + InformerEvent event = new InformerEvent(action, object, oldObject); this.eventHandler.handleEvent(event); }); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParser.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParser.java new file mode 100644 index 0000000000..9c2061b642 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParser.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class LabelSelectorParser { + + public static Map parseSimpleLabelSelector(String labelSelector) { + if (labelSelector == null || labelSelector.isBlank()) { + return Collections.EMPTY_MAP; + } + String[] selectors = labelSelector.split(","); + Map labels = new HashMap<>(selectors.length); + Arrays.stream(selectors).forEach(s -> { + var kv = s.split("="); + labels.put(kv[0], kv[1]); + }); + return labels; + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java index dc0b4b4501..7351b80d4f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java @@ -5,27 +5,42 @@ import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; public class Mappers { - public static Function> fromAnnotation( - String annotationKey) { - return fromMetadata(annotationKey, false); + + public static Function> fromAnnotation( + String nameKey) { + return fromMetadata(nameKey, null, false); + } + + public static Function> fromAnnotation( + String nameKey, String namespaceKey) { + return fromMetadata(nameKey, namespaceKey, false); + } + + public static Function> fromLabel( + String nameKey) { + return fromMetadata(nameKey, null, true); } - public static Function> fromLabel( - String labelKey) { - return fromMetadata(labelKey, true); + public static Function> fromLabel( + String nameKey, String namespaceKey) { + return fromMetadata(nameKey, namespaceKey, true); } - private static Function> fromMetadata( - String key, boolean isLabel) { + private static Function> fromMetadata( + String nameKey, String namespaceKey, boolean isLabel) { return resource -> { final var metadata = resource.getMetadata(); if (metadata == null) { return Collections.emptySet(); } else { final var map = isLabel ? metadata.getLabels() : metadata.getAnnotations(); - return map != null ? Set.of(map.get(key)) : Collections.emptySet(); + var namespace = + namespaceKey == null ? resource.getMetadata().getNamespace() : map.get(namespaceKey); + return map != null ? Set.of(new CustomResourceID(map.get(nameKey), namespace)) + : Collections.emptySet(); } }; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceAction.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceAction.java new file mode 100644 index 0000000000..6fbc233133 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceAction.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +public enum ResourceAction { + ADDED, UPDATED, DELETED +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEvent.java index 6fa38674bf..9e9bfb5040 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEvent.java @@ -1,10 +1,11 @@ package io.javaoperatorsdk.operator.processing.event.internal; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; public class TimerEvent extends DefaultEvent { - public TimerEvent(String relatedCustomResourceUid, TimerEventSource eventSource) { - super(relatedCustomResourceUid, eventSource); + public TimerEvent(CustomResourceID relatedCustomResourceUid) { + super(relatedCustomResourceUid); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index 9ad15e47f9..51f21fc4d4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -11,23 +11,23 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; public class TimerEventSource> extends AbstractEventSource { private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class); private final Timer timer = new Timer(); private final AtomicBoolean running = new AtomicBoolean(); - private final Map onceTasks = new ConcurrentHashMap<>(); - private final Map timerTasks = new ConcurrentHashMap<>(); + private final Map onceTasks = new ConcurrentHashMap<>(); + private final Map timerTasks = new ConcurrentHashMap<>(); public void schedule(R customResource, long delay, long period) { if (!running.get()) { throw new IllegalStateException("The TimerEventSource is not running"); } - String resourceUid = KubernetesResourceUtils.getUID(customResource); + CustomResourceID resourceUid = CustomResourceID.fromResource(customResource); if (timerTasks.containsKey(resourceUid)) { return; } @@ -40,8 +40,7 @@ public void scheduleOnce(R customResource, long delay) { if (!running.get()) { throw new IllegalStateException("The TimerEventSource is not running"); } - - String resourceUid = KubernetesResourceUtils.getUID(customResource); + CustomResourceID resourceUid = CustomResourceID.fromResource(customResource); if (onceTasks.containsKey(resourceUid)) { cancelOnceSchedule(resourceUid); } @@ -51,19 +50,19 @@ public void scheduleOnce(R customResource, long delay) { } @Override - public void eventSourceDeRegisteredForResource(String customResourceUid) { + public void eventSourceDeRegisteredForResource(CustomResourceID customResourceUid) { cancelSchedule(customResourceUid); cancelOnceSchedule(customResourceUid); } - public void cancelSchedule(String customResourceUid) { - TimerTask timerTask = timerTasks.remove(customResourceUid); + public void cancelSchedule(CustomResourceID customResourceID) { + TimerTask timerTask = timerTasks.remove(customResourceID); if (timerTask != null) { timerTask.cancel(); } } - public void cancelOnceSchedule(String customResourceUid) { + public void cancelOnceSchedule(CustomResourceID customResourceUid) { TimerTask timerTask = onceTasks.remove(customResourceUid); if (timerTask != null) { timerTask.cancel(); @@ -85,9 +84,9 @@ public void close() throws IOException { public class EventProducerTimeTask extends TimerTask { - protected final String customResourceUid; + protected final CustomResourceID customResourceUid; - public EventProducerTimeTask(String customResourceUid) { + public EventProducerTimeTask(CustomResourceID customResourceUid) { this.customResourceUid = customResourceUid; } @@ -95,7 +94,7 @@ public EventProducerTimeTask(String customResourceUid) { public void run() { if (running.get()) { log.debug("Producing event for custom resource id: {}", customResourceUid); - eventHandler.handleEvent(new TimerEvent(customResourceUid, TimerEventSource.this)); + eventHandler.handleEvent(new TimerEvent(customResourceUid)); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index 93e6e57492..c1e887de86 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -6,16 +6,14 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionBuilder; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceSpec; public class TestUtils { - public static final String TEST_CUSTOM_RESOURCE_NAME = "test-custom-resource"; - public static final String TEST_NAMESPACE = "java-operator-sdk-int-test"; - public static TestCustomResource testCustomResource() { - return testCustomResource(UUID.randomUUID().toString()); + return testCustomResource(new CustomResourceID(UUID.randomUUID().toString(), "test")); } public static CustomResourceDefinition testCRD(String scope) { @@ -29,14 +27,13 @@ public static CustomResourceDefinition testCRD(String scope) { .build(); } - public static TestCustomResource testCustomResource(String uid) { + public static TestCustomResource testCustomResource(CustomResourceID id) { TestCustomResource resource = new TestCustomResource(); resource.setMetadata( new ObjectMetaBuilder() - .withName(TEST_CUSTOM_RESOURCE_NAME) - .withUid(uid) + .withName(id.getName()) .withGeneration(1L) - .withNamespace(TEST_NAMESPACE) + .withNamespace(id.getNamespace().orElse(null)) .build()); resource.getMetadata().setAnnotations(new HashMap<>()); resource.setKind("CustomService"); @@ -46,4 +43,6 @@ public static TestCustomResource testCustomResource(String uid) { resource.getSpec().setValue("test-value"); return resource; } + + } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java deleted file mode 100644 index 33f8347f13..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/CustomResourceSelectorTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package io.javaoperatorsdk.operator.processing; - -import java.util.Objects; -import java.util.UUID; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import io.javaoperatorsdk.operator.processing.event.DefaultEvent; -import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; - -import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -class CustomResourceSelectorTest { - - public static final int SEPARATE_EXECUTION_TIMEOUT = 450; - - private final EventDispatcher eventDispatcherMock = mock(EventDispatcher.class); - private final CustomResourceCache customResourceCache = new CustomResourceCache(); - - private final DefaultEventSourceManager defaultEventSourceManagerMock = - mock(DefaultEventSourceManager.class); - - private final DefaultEventHandler defaultEventHandler = - new DefaultEventHandler(eventDispatcherMock, "Test", null); - - @BeforeEach - public void setup() { - defaultEventHandler.setEventSourceManager(defaultEventSourceManagerMock); - - // todo: remove - when(defaultEventSourceManagerMock.getCache()).thenReturn(customResourceCache); - doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResource(any()); - doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResources(any()); - doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResourceUids(any()); - doCallRealMethod().when(defaultEventSourceManagerMock).cacheResource(any(), any()); - doAnswer( - invocation -> { - final var resourceId = (String) invocation.getArgument(0); - customResourceCache.cleanup(resourceId); - return null; - }) - .when(defaultEventSourceManagerMock) - .cleanup(any()); - } - - @Test - public void dispatchEventsWithPredicate() { - TestCustomResource cr1 = testCustomResource(UUID.randomUUID().toString()); - cr1.getSpec().setValue("1"); - TestCustomResource cr2 = testCustomResource(UUID.randomUUID().toString()); - cr2.getSpec().setValue("2"); - TestCustomResource cr3 = testCustomResource(UUID.randomUUID().toString()); - cr3.getSpec().setValue("3"); - - customResourceCache.cacheResource(cr1); - customResourceCache.cacheResource(cr2); - customResourceCache.cacheResource(cr3); - - defaultEventHandler.handleEvent( - new DefaultEvent( - c -> { - var tcr = ((TestCustomResource) c); - return Objects.equals("1", tcr.getSpec().getValue()) - || Objects.equals("3", tcr.getSpec().getValue()); - }, - null)); - - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) - .handleExecution(any()); - - waitMinimalTime(); - - ArgumentCaptor executionScopeArgumentCaptor = - ArgumentCaptor.forClass(ExecutionScope.class); - - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) - .handleExecution(executionScopeArgumentCaptor.capture()); - - assertThat(executionScopeArgumentCaptor.getAllValues()) - .hasSize(2) - .allSatisfy( - s -> { - assertThat(s.getEvents()).isNotEmpty().hasOnlyElementsOfType(DefaultEvent.class); - assertThat(s) - .satisfiesAnyOf( - e -> Objects.equals(cr1.getMetadata().getUid(), e.getCustomResourceUid()), - e -> Objects.equals(cr3.getMetadata().getUid(), e.getCustomResourceUid())); - }); - } - - private void waitMinimalTime() { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new IllegalStateException(e); - } - } - -} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index a03f4bd8a1..620a7c9ac7 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing; -import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; @@ -11,21 +11,22 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.Watcher; +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; +import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.DELETED; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; @@ -39,18 +40,19 @@ class DefaultEventHandlerTest { public static final int FAKE_CONTROLLER_EXECUTION_DURATION = 250; public static final int SEPARATE_EXECUTION_TIMEOUT = 450; + public static final String TEST_NAMESPACE = "default-event-handler-test"; private EventDispatcher eventDispatcherMock = mock(EventDispatcher.class); - private CustomResourceCache customResourceCache = new CustomResourceCache(); private DefaultEventSourceManager defaultEventSourceManagerMock = mock(DefaultEventSourceManager.class); + private ResourceCache resourceCache = mock(ResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private DefaultEventHandler defaultEventHandler = - new DefaultEventHandler(eventDispatcherMock, "Test", null); + new DefaultEventHandler(eventDispatcherMock, resourceCache, "Test", null); private DefaultEventHandler defaultEventHandlerWithRetry = - new DefaultEventHandler(eventDispatcherMock, "Test", + new DefaultEventHandler(eventDispatcherMock, resourceCache, "Test", GenericRetry.defaultLimitedExponentialRetry()); @BeforeEach @@ -59,22 +61,6 @@ public void setup() { .thenReturn(retryTimerEventSourceMock); defaultEventHandler.setEventSourceManager(defaultEventSourceManagerMock); defaultEventHandlerWithRetry.setEventSourceManager(defaultEventSourceManagerMock); - - // todo: remove - when(defaultEventSourceManagerMock.getCache()).thenReturn(customResourceCache); - doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResource(any()); - doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResource(any()); - doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResources(any()); - doCallRealMethod().when(defaultEventSourceManagerMock).getLatestResourceUids(any()); - doCallRealMethod().when(defaultEventSourceManagerMock).cacheResource(any(), any()); - doAnswer( - invocation -> { - final var resourceId = (String) invocation.getArgument(0); - customResourceCache.cleanup(resourceId); - return null; - }) - .when(defaultEventSourceManagerMock) - .cleanup(any()); } @Test @@ -87,7 +73,8 @@ public void dispatchesEventsIfNoExecutionInProgress() { @Test public void skipProcessingIfLatestCustomResourceNotInCache() { Event event = prepareCREvent(); - customResourceCache.cleanup(event.getRelatedCustomResourceUid()); + when(resourceCache.getCustomResource(event.getRelatedCustomResourceID())) + .thenReturn(Optional.empty()); defaultEventHandler.handleEvent(event); @@ -96,7 +83,7 @@ public void skipProcessingIfLatestCustomResourceNotInCache() { @Test public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedException { - String resourceUid = eventAlreadyUnderProcessing(); + CustomResourceID resourceUid = eventAlreadyUnderProcessing(); defaultEventHandler.handleEvent(nonCREvent(resourceUid)); @@ -106,7 +93,7 @@ public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedExcep @Test public void buffersAllIncomingEventsWhileControllerInExecution() { - String resourceUid = eventAlreadyUnderProcessing(); + CustomResourceID resourceUid = eventAlreadyUnderProcessing(); defaultEventHandler.handleEvent(nonCREvent(resourceUid)); defaultEventHandler.handleEvent(prepareCREvent(resourceUid)); @@ -123,17 +110,16 @@ public void buffersAllIncomingEventsWhileControllerInExecution() { @Test public void cleanUpAfterDeleteEvent() { TestCustomResource customResource = testCustomResource(); - customResourceCache.cacheResource(customResource); + when(resourceCache.getCustomResource(CustomResourceID.fromResource(customResource))) + .thenReturn(Optional.of(customResource)); CustomResourceEvent event = - new CustomResourceEvent(Watcher.Action.DELETED, customResource, null); - String uid = customResource.getMetadata().getUid(); + new CustomResourceEvent(DELETED, customResource); defaultEventHandler.handleEvent(event); waitMinimalTime(); - - verify(defaultEventSourceManagerMock, times(1)).cleanup(uid); - assertThat(customResourceCache.getLatestResource(uid)).isNotPresent(); + verify(defaultEventSourceManagerMock, times(1)) + .cleanup(CustomResourceID.fromResource(customResource)); } @Test @@ -141,7 +127,7 @@ public void schedulesAnEventRetryOnException() { Event event = prepareCREvent(); TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(Arrays.asList(event), customResource, null); + ExecutionScope executionScope = new ExecutionScope(List.of(event), customResource, null); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); @@ -155,8 +141,7 @@ public void schedulesAnEventRetryOnException() { public void executesTheControllerInstantlyAfterErrorIfEventsBuffered() { Event event = prepareCREvent(); TestCustomResource customResource = testCustomResource(); - customResource.getMetadata().setUid(event.getRelatedCustomResourceUid()); - ExecutionScope executionScope = new ExecutionScope(Arrays.asList(event), customResource, null); + overrideData(event.getRelatedCustomResourceID(), customResource); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); @@ -186,7 +171,7 @@ public void successfulExecutionResetsTheRetry() { Event event = prepareCREvent(); TestCustomResource customResource = testCustomResource(); - customResource.getMetadata().setUid(event.getRelatedCustomResourceUid()); + overrideData(event.getRelatedCustomResourceID(), customResource); PostExecutionControl postExecutionControlWithException = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); PostExecutionControl defaultDispatchControl = PostExecutionControl.defaultDispatch(); @@ -222,7 +207,7 @@ public void successfulExecutionResetsTheRetry() { @Test public void scheduleTimedEventIfInstructedByPostExecutionControl() { - var testDelay = 10000l; + var testDelay = 10000L; when(eventDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); @@ -234,7 +219,7 @@ public void scheduleTimedEventIfInstructedByPostExecutionControl() { @Test public void reScheduleOnlyIfNotExecutedBufferedEvents() { - var testDelay = 10000l; + var testDelay = 10000L; when(eventDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); @@ -261,7 +246,7 @@ private void waitMinimalTime() { } } - private String eventAlreadyUnderProcessing() { + private CustomResourceID eventAlreadyUnderProcessing() { when(eventDispatcherMock.handleExecution(any())) .then( (Answer) invocationOnMock -> { @@ -270,21 +255,26 @@ private String eventAlreadyUnderProcessing() { }); Event event = prepareCREvent(); defaultEventHandler.handleEvent(event); - return event.getRelatedCustomResourceUid(); + return event.getRelatedCustomResourceID(); } private CustomResourceEvent prepareCREvent() { - return prepareCREvent(UUID.randomUUID().toString()); + return prepareCREvent(new CustomResourceID(UUID.randomUUID().toString(), TEST_NAMESPACE)); } - private CustomResourceEvent prepareCREvent(String uid) { + private CustomResourceEvent prepareCREvent(CustomResourceID uid) { TestCustomResource customResource = testCustomResource(uid); - customResourceCache.cacheResource(customResource); - return new CustomResourceEvent(Watcher.Action.MODIFIED, customResource, null); + when(resourceCache.getCustomResource(eq(uid))).thenReturn(Optional.of(customResource)); + return new CustomResourceEvent(ResourceAction.UPDATED, customResource); } - private Event nonCREvent(String relatedCustomResourceUid) { - TimerEvent timerEvent = new TimerEvent(relatedCustomResourceUid, null); - return timerEvent; + private Event nonCREvent(CustomResourceID relatedCustomResourceUid) { + return new TimerEvent(relatedCustomResourceUid); } + + private void overrideData(CustomResourceID id, CustomResource applyTo) { + applyTo.getMetadata().setName(id.getName()); + applyTo.getMetadata().setNamespace(id.getNamespace().orElse(null)); + } + } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java index c9b414f52b..6dc16f8a69 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; @@ -14,17 +15,18 @@ class EventBufferTest { private EventBuffer eventBuffer = new EventBuffer(); - String uid = UUID.randomUUID().toString(); - Event testEvent1 = new TimerEvent(uid, null); - Event testEvent2 = new TimerEvent(uid, null); + String name = UUID.randomUUID().toString(); + CustomResourceID customResourceID = new CustomResourceID(name); + Event testEvent1 = new TimerEvent(customResourceID); + Event testEvent2 = new TimerEvent(customResourceID); @Test public void storesEvents() { eventBuffer.addEvent(testEvent1); eventBuffer.addEvent(testEvent2); - assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceUid())).isTrue(); - List events = eventBuffer.getAndRemoveEventsForExecution(uid); + assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceID())).isTrue(); + List events = eventBuffer.getAndRemoveEventsForExecution(customResourceID); assertThat(events).hasSize(2); } @@ -33,7 +35,7 @@ public void getsAndRemovesEvents() { eventBuffer.addEvent(testEvent1); eventBuffer.addEvent(testEvent2); - List events = eventBuffer.getAndRemoveEventsForExecution(uid); + List events = eventBuffer.getAndRemoveEventsForExecution(new CustomResourceID(name)); assertThat(events).hasSize(2); assertThat(events).contains(testEvent1, testEvent2); } @@ -43,7 +45,7 @@ public void checksIfThereAreStoredEvents() { eventBuffer.addEvent(testEvent1); eventBuffer.addEvent(testEvent2); - assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceUid())).isTrue(); + assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceID())).isTrue(); } @Test @@ -51,8 +53,8 @@ public void canClearEvents() { eventBuffer.addEvent(testEvent1); eventBuffer.addEvent(testEvent2); - eventBuffer.cleanup(uid); + eventBuffer.cleanup(customResourceID); - assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceUid())).isFalse(); + assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceID())).isFalse(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index 2f9de4de82..2a8ad4b0b9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -10,7 +10,6 @@ import org.mockito.ArgumentMatchers; import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.Watcher; import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.Context; @@ -22,7 +21,10 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; +import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.ADDED; +import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.UPDATED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -72,7 +74,7 @@ void setup() { void addFinalizerOnNewResource() { assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.ADDED, testCustomResource)); + executionScopeWithCREvent(ADDED, testCustomResource)); verify(controller, never()) .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) @@ -85,7 +87,7 @@ void addFinalizerOnNewResource() { void callCreateOrUpdateOnNewResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.ADDED, testCustomResource)); + executionScopeWithCREvent(ADDED, testCustomResource)); verify(controller, times(1)) .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); } @@ -98,7 +100,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { .thenReturn(UpdateControl.updateStatusSubResource(testCustomResource)); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.ADDED, testCustomResource)); + executionScopeWithCREvent(ADDED, testCustomResource)); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); verify(customResourceFacade, never()).replaceWithLock(any()); @@ -113,7 +115,7 @@ void updatesBothResourceAndStatusIfFinalizerSet() { when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); verify(customResourceFacade, times(1)).replaceWithLock(testCustomResource); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); @@ -124,7 +126,7 @@ void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); verify(controller, times(1)) .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); } @@ -136,7 +138,7 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { markForDeletion(testCustomResource); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); verify(controller, times(1)).deleteResource(eq(testCustomResource), any()); } @@ -148,7 +150,7 @@ void callDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { markForDeletion(testCustomResource); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); verify(controller).deleteResource(eq(testCustomResource), any()); } @@ -158,7 +160,7 @@ void doNotCallDeleteIfMarkedForDeletionWhenFinalizerHasAlreadyBeenRemoved() { markForDeletion(testCustomResource); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); verify(controller, never()).deleteResource(eq(testCustomResource), any()); } @@ -178,7 +180,7 @@ void doesNotAddFinalizerIfConfiguredNotTo() { configureToNotUseFinalizer(); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); assertEquals(0, testCustomResource.getMetadata().getFinalizers().size()); } @@ -189,7 +191,7 @@ void removesDefaultFinalizerOnDeleteIfSet() { markForDeletion(testCustomResource); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); assertEquals(0, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, times(1)).replaceWithLock(any()); @@ -204,7 +206,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { markForDeletion(testCustomResource); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, never()).replaceWithLock(any()); @@ -218,7 +220,7 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { .thenReturn(UpdateControl.noUpdate()); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); verify(customResourceFacade, never()).updateStatus(testCustomResource); } @@ -230,7 +232,7 @@ void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { .thenReturn(UpdateControl.noUpdate()); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, times(1)).replaceWithLock(any()); @@ -242,7 +244,7 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { markForDeletion(testCustomResource); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); verify(controller, never()).deleteResource(eq(testCustomResource), any()); @@ -252,9 +254,9 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.MODIFIED, testCustomResource)); + executionScopeWithCREvent(UPDATED, testCustomResource)); verify(controller, times(2)).createOrUpdateResource(eq(testCustomResource), any()); } @@ -265,7 +267,7 @@ void propagatesRetryInfoToContextIfFinalizerSet() { eventDispatcher.handleExecution( new ExecutionScope( - Arrays.asList(), + List.of(), testCustomResource, new RetryInfo() { @Override @@ -295,12 +297,12 @@ void setReScheduleToPostExecutionControlFromUpdateControl() { when(controller.createOrUpdateResource(eq(testCustomResource), any())) .thenReturn( - UpdateControl.updateStatusSubResource(testCustomResource).withReSchedule(1000l)); + UpdateControl.updateStatusSubResource(testCustomResource).withReSchedule(1000L)); PostExecutionControl control = eventDispatcher.handleExecution( - executionScopeWithCREvent(Watcher.Action.ADDED, testCustomResource)); + executionScopeWithCREvent(ADDED, testCustomResource)); - assertThat(control.getReScheduleDelay().get()).isEqualTo(1000l); + assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); } private void markForDeletion(CustomResource customResource) { @@ -312,8 +314,8 @@ private void removeFinalizers(CustomResource customResource) { } public ExecutionScope executionScopeWithCREvent( - Watcher.Action action, CustomResource resource, Event... otherEvents) { - CustomResourceEvent event = new CustomResourceEvent(action, resource, null); + ResourceAction action, CustomResource resource, Event... otherEvents) { + CustomResourceEvent event = new CustomResourceEvent(action, resource); List eventList = new ArrayList<>(1 + otherEvents.length); eventList.add(event); eventList.addAll(Arrays.asList(otherEvents)); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java index 8862450d06..089215486e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java @@ -8,9 +8,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.DefaultEventHandler; -import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.eq; @@ -24,7 +22,7 @@ class DefaultEventSourceManagerTest { private DefaultEventHandler defaultEventHandlerMock = mock(DefaultEventHandler.class); private DefaultEventSourceManager defaultEventSourceManager = - new DefaultEventSourceManager(defaultEventHandlerMock, false); + new DefaultEventSourceManager(defaultEventHandlerMock); @Test public void registersEventSource() { @@ -34,7 +32,7 @@ public void registersEventSource() { Map registeredSources = defaultEventSourceManager.getRegisteredEventSources(); - assertThat(registeredSources.entrySet()).hasSize(1); + assertThat(registeredSources.entrySet()).hasSize(2); assertThat(registeredSources.get(CUSTOM_EVENT_SOURCE_NAME)).isEqualTo(eventSource); verify(eventSource, times(1)).setEventHandler(eq(defaultEventHandlerMock)); verify(eventSource, times(1)).start(); @@ -61,9 +59,8 @@ public void throwExceptionIfRegisteringEventSourceWithSameName() { defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, eventSource); assertThatExceptionOfType(IllegalStateException.class) .isThrownBy( - () -> { - defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, eventSource2); - }); + () -> defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, + eventSource2)); } @Test @@ -73,9 +70,9 @@ public void deRegistersEventSources() { defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, eventSource); defaultEventSourceManager.deRegisterCustomResourceFromEventSource( - CUSTOM_EVENT_SOURCE_NAME, getUID(customResource)); + CUSTOM_EVENT_SOURCE_NAME, CustomResourceID.fromResource(customResource)); verify(eventSource, times(1)) - .eventSourceDeRegisteredForResource(eq(KubernetesResourceUtils.getUID(customResource))); + .eventSourceDeRegisteredForResource(eq(CustomResourceID.fromResource(customResource))); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java index 2f3dba65a5..dc65981cff 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java @@ -13,10 +13,11 @@ class EventListTest { @Test public void returnsLatestOfEventType() { - TimerEvent event2 = new TimerEvent("1", null); + TimerEvent event2 = new TimerEvent(new CustomResourceID("name1")); EventList eventList = new EventList( - Arrays.asList(mock(Event.class), new TimerEvent("2", null), event2, mock(Event.class))); + Arrays.asList(mock(Event.class), new TimerEvent(new CustomResourceID("name2")), event2, + mock(Event.class))); assertThat(eventList.getLatestOfType(TimerEvent.class).get()).isEqualTo(event2); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java index ab41b833ff..bd0d2ed72d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; @@ -52,13 +51,13 @@ public void eventFilteredByCustomPredicate() { cr.getMetadata().setGeneration(1L); cr.getStatus().setConfigMapStatus("1"); - eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + eventSource.eventReceived(ResourceAction.UPDATED, cr, null); verify(eventHandler, times(1)).handleEvent(any()); cr.getMetadata().setGeneration(1L); cr.getStatus().setConfigMapStatus("1"); - eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + eventSource.eventReceived(ResourceAction.UPDATED, cr, cr); verify(eventHandler, times(1)).handleEvent(any()); } @@ -80,13 +79,18 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { cr.getMetadata().setGeneration(1L); cr.getStatus().setConfigMapStatus("1"); - eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + TestCustomResource cr2 = TestUtils.testCustomResource(); + cr.getMetadata().setFinalizers(List.of(FINALIZER)); + cr.getMetadata().setGeneration(2L); + cr.getStatus().setConfigMapStatus("1"); + + eventSource.eventReceived(ResourceAction.UPDATED, cr, cr2); verify(eventHandler, times(1)).handleEvent(any()); cr.getMetadata().setGeneration(1L); cr.getStatus().setConfigMapStatus("2"); - eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + eventSource.eventReceived(ResourceAction.UPDATED, cr, cr); verify(eventHandler, times(1)).handleEvent(any()); } @@ -95,11 +99,9 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { var config = new TestControllerConfig( FINALIZER, false, - (configuration, oldResource, newResource) -> { - return !Objects.equals( - oldResource.getStatus().getConfigMapStatus(), - newResource.getStatus().getConfigMapStatus()); - }); + (configuration, oldResource, newResource) -> !Objects.equals( + oldResource.getStatus().getConfigMapStatus(), + newResource.getStatus().getConfigMapStatus())); when(config.getConfigurationService().getObjectMapper()) .thenReturn(ConfigurationService.OBJECT_MAPPER); @@ -112,13 +114,13 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { cr.getMetadata().setGeneration(1L); cr.getStatus().setConfigMapStatus("1"); - eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + eventSource.eventReceived(ResourceAction.UPDATED, cr, cr); verify(eventHandler, times(1)).handleEvent(any()); cr.getMetadata().setGeneration(1L); cr.getStatus().setConfigMapStatus("1"); - eventSource.eventReceived(Watcher.Action.MODIFIED, cr); + eventSource.eventReceived(ResourceAction.UPDATED, cr, cr); verify(eventHandler, times(2)).handleEvent(any()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 727caf3be1..4bf7d1afd7 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.Metrics; @@ -41,13 +40,19 @@ public void setup() { @Test public void skipsEventHandlingIfGenerationNotIncreased() { - TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResource1.getMetadata().setFinalizers(List.of(FINALIZER)); + TestCustomResource customResource = TestUtils.testCustomResource(); + customResource.getMetadata().setFinalizers(List.of(FINALIZER)); + customResource.getMetadata().setGeneration(2L); + + TestCustomResource oldCustomResource = TestUtils.testCustomResource(); + oldCustomResource.getMetadata().setFinalizers(List.of(FINALIZER)); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, + oldCustomResource); verify(eventHandler, times(1)).handleEvent(any()); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, + customResource); verify(eventHandler, times(1)).handleEvent(any()); } @@ -55,12 +60,14 @@ public void skipsEventHandlingIfGenerationNotIncreased() { public void dontSkipEventHandlingIfMarkedForDeletion() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); verify(eventHandler, times(1)).handleEvent(any()); // mark for deletion customResource1.getMetadata().setDeletionTimestamp(LocalDateTime.now().toString()); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @@ -68,11 +75,13 @@ public void dontSkipEventHandlingIfMarkedForDeletion() { public void normalExecutionIfGenerationChanges() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); verify(eventHandler, times(1)).handleEvent(any()); customResource1.getMetadata().setGeneration(2L); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @@ -84,10 +93,12 @@ public void handlesAllEventIfNotGenerationAware() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); verify(eventHandler, times(1)).handleEvent(any()); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @@ -95,10 +106,12 @@ public void handlesAllEventIfNotGenerationAware() { public void eventNotMarkedForLastGenerationIfNoFinalizer() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); verify(eventHandler, times(1)).handleEvent(any()); - customResourceEventSource.eventReceived(Watcher.Action.MODIFIED, customResource1); + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); verify(eventHandler, times(2)).handleEvent(any()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index b6094a0ffe..7556d2412a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -4,6 +4,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -11,6 +12,7 @@ import org.awaitility.core.ConditionTimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.internal.util.collections.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +44,7 @@ public class CustomResourceSelectorTest { private static final Logger LOGGER = LoggerFactory.getLogger(CustomResourceSelectorTest.class); + public static final String NAMESPACE = "test"; KubernetesMockServer server; KubernetesClient client; @@ -112,11 +115,13 @@ void resourceWatchedByLabel() { new MyConfiguration(configurationService, "app=bar")); o2.start(); - client.resources(TestCustomResource.class).inNamespace("test").create(newMyResource("foo")); - client.resources(TestCustomResource.class).inNamespace("test").create(newMyResource("bar")); + client.resources(TestCustomResource.class).inNamespace(NAMESPACE).create(newMyResource("foo", + NAMESPACE)); + client.resources(TestCustomResource.class).inNamespace(NAMESPACE).create(newMyResource("bar", + NAMESPACE)); await() - .atMost(5, TimeUnit.SECONDS) + .atMost(325, TimeUnit.SECONDS) .pollInterval(100, TimeUnit.MILLISECONDS) .until(() -> c1.get() == 1 && c1err.get() == 0); await() @@ -133,9 +138,10 @@ void resourceWatchedByLabel() { } } - public TestCustomResource newMyResource(String app) { + public TestCustomResource newMyResource(String app, String namespace) { TestCustomResource resource = new TestCustomResource(); resource.setMetadata(new ObjectMetaBuilder().withName(app).addToLabels("app", app).build()); + resource.getMetadata().setNamespace(namespace); return resource; } @@ -159,13 +165,18 @@ public String getAssociatedControllerClassName() { return MyController.class.getCanonicalName(); } + @Override + public Set getNamespaces() { + return Sets.newSet(NAMESPACE); + } + @Override public ConfigurationService getConfigurationService() { return service; } } - @Controller + @Controller(namespaces = NAMESPACE) public static class MyController implements ResourceController { private final Consumer consumer; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParserTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParserTest.java new file mode 100644 index 0000000000..3a619bbc19 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParserTest.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class LabelSelectorParserTest { + + @Test + public void nullParamReturnsEmptyMap() { + var res = LabelSelectorParser.parseSimpleLabelSelector(null); + assertThat(res).hasSize(0); + } + + @Test + public void emptyLabelSelectorReturnsEmptyMap() { + var res = LabelSelectorParser.parseSimpleLabelSelector(" "); + assertThat(res).hasSize(0); + } + + @Test + public void parseSimpleLabelSelector() { + var res = LabelSelectorParser.parseSimpleLabelSelector("app=foo"); + assertThat(res).hasSize(1).containsEntry("app", "foo"); + + res = LabelSelectorParser.parseSimpleLabelSelector("app=foo,owner=me"); + assertThat(res).hasSize(2).containsEntry("app", "foo").containsEntry("owner", "me"); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index 892c422fc1..f7f2814255 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -12,12 +12,11 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.processing.KubernetesResourceUtils; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -48,9 +47,9 @@ public void producesEventsPeriodically() { assertThat(eventHandlerMock.events) .hasSizeGreaterThan(2); assertThat(eventHandlerMock.events) - .allMatch(e -> e.getRelatedCustomResourceUid().equals(getUID(customResource))); - assertThat(eventHandlerMock.events) - .allMatch(e -> e.getEventSource().equals(timerEventSource)); + .allMatch(e -> e.getRelatedCustomResourceID() + .equals(CustomResourceID.fromResource(customResource))); + }); } @@ -61,7 +60,8 @@ public void deRegistersPeriodicalEventSources() { timerEventSource.schedule(customResource, INITIAL_DELAY, PERIOD); untilAsserted(() -> assertThat(eventHandlerMock.events).hasSizeGreaterThan(1)); - timerEventSource.eventSourceDeRegisteredForResource(getUID(customResource)); + timerEventSource + .eventSourceDeRegisteredForResource(CustomResourceID.fromResource(customResource)); int size = eventHandlerMock.events.size(); untilAsserted(() -> assertThat(eventHandlerMock.events).hasSize(size)); @@ -82,7 +82,7 @@ public void canCancelOnce() { TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); - timerEventSource.cancelOnceSchedule(KubernetesResourceUtils.getUID(customResource)); + timerEventSource.cancelOnceSchedule(CustomResourceID.fromResource(customResource)); untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } @@ -102,7 +102,8 @@ public void deRegistersOnceEventSources() { TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); - timerEventSource.eventSourceDeRegisteredForResource(getUID(customResource)); + timerEventSource + .eventSourceDeRegisteredForResource(CustomResourceID.fromResource(customResource)); untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 743e2c7b72..b48aca918b 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -67,6 +67,11 @@ private OperatorExtension( this.waitForNamespaceDeletion = waitForNamespaceDeletion; } + /** + * Creates a {@link Builder} to set up an {@link OperatorExtension} instance. + * + * @return the builder. + */ public static Builder builder() { return new Builder(); } @@ -123,7 +128,7 @@ public NonNamespaceOperation T getNamedResource(Class type, String name) { + public T get(Class type, String name) { return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); } @@ -135,10 +140,6 @@ public T replace(Class type, T resource) { return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); } - public T get(Class type, String name) { - return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); - } - @SuppressWarnings("unchecked") protected void before(ExtensionContext context) { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java index 279167fc22..67cc71ac5a 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java @@ -18,7 +18,7 @@ */ class AccumulativeMappingWriter { - private Map mappings = new ConcurrentHashMap<>(); + private final Map mappings = new ConcurrentHashMap<>(); private final String resourcePath; private final ProcessingEnvironment processingEnvironment; diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 04502d217f..ba4cf40250 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,8 +1,7 @@ package io.javaoperatorsdk.operator.config.runtime; -import java.util.Optional; import java.util.Set; -import java.util.function.Predicate; +import java.util.function.Function; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; @@ -17,12 +16,12 @@ public class AnnotationConfiguration implements ControllerConfiguration { private final ResourceController controller; - private final Optional annotation; + private final Controller annotation; private ConfigurationService service; public AnnotationConfiguration(ResourceController controller) { this.controller = controller; - this.annotation = Optional.ofNullable(controller.getClass().getAnnotation(Controller.class)); + this.annotation = controller.getClass().getAnnotation(Controller.class); } @Override @@ -32,15 +31,16 @@ public String getName() { @Override public String getFinalizer() { - return annotation - .map(Controller::finalizerName) - .filter(Predicate.not(String::isBlank)) - .orElse(ControllerUtils.getDefaultFinalizerName(getCRDName())); + if (annotation == null || annotation.finalizerName().isBlank()) { + return ControllerUtils.getDefaultFinalizerName(getCRDName()); + } else { + return annotation.finalizerName(); + } } @Override public boolean isGenerationAware() { - return annotation.map(Controller::generationAwareEventProcessing).orElse(true); + return valueOrDefault(annotation, Controller::generationAwareEventProcessing, true); } @Override @@ -50,12 +50,12 @@ public Class getCustomResourceClass() { @Override public Set getNamespaces() { - return Set.of(annotation.map(Controller::namespaces).orElse(new String[] {})); + return Set.of(valueOrDefault(annotation, Controller::namespaces, new String[] {})); } @Override public String getLabelSelector() { - return annotation.map(Controller::labelSelector).orElse(""); + return valueOrDefault(annotation, Controller::labelSelector, ""); } @Override @@ -78,9 +78,11 @@ public String getAssociatedControllerClassName() { public CustomResourceEventFilter getEventFilter() { CustomResourceEventFilter answer = null; - var filterTypes = annotation.map(Controller::eventFilters); - if (filterTypes.isPresent()) { - for (var filterType : filterTypes.get()) { + Class>[] filterTypes = + (Class>[]) valueOrDefault(annotation, Controller::eventFilters, + new Object[] {}); + if (filterTypes.length > 0) { + for (var filterType : filterTypes) { try { CustomResourceEventFilter filter = filterType.getConstructor().newInstance(); @@ -94,10 +96,18 @@ public CustomResourceEventFilter getEventFilter() { } } } - return answer != null ? answer : CustomResourceEventFilters.passthrough(); } + + public static T valueOrDefault(Controller controller, Function mapper, + T defaultValue) { + if (controller == null) { + return defaultValue; + } else { + return mapper.apply(controller); + } + } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java index 1258904394..d76db8fbcd 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java @@ -36,7 +36,7 @@ static Map provide(final String resourcePath, T key, V value) { throw new IllegalStateException( String.format( "%s is not valid Mapping metadata, defined in %s", - clazzPair, url.toString())); + clazzPair, url)); } result.put( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index 6fa248a3d9..4332faf413 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -58,7 +58,7 @@ public void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedExcepti // update some resources for (int i = 0; i < NUMBER_OF_RESOURCES_UPDATED; i++) { TestCustomResource tcr = - operator.getNamedResource(TestCustomResource.class, + operator.get(TestCustomResource.class, TestUtils.TEST_CUSTOM_RESOURCE_PREFIX + i); tcr.getSpec().setValue(i + UPDATED_SUFFIX); operator.resources(TestCustomResource.class) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index 4855e61f49..cb3cdbb704 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -52,7 +52,7 @@ void awaitResourcesCreatedOrUpdated() { .untilAsserted( () -> { ConfigMap configMap = - operator.getNamedResource(ConfigMap.class, "test-config-map"); + operator.get(ConfigMap.class, "test-config-map"); assertThat(configMap).isNotNull(); assertThat(configMap.getData().get("test-key")).isEqualTo("test-value"); }); @@ -68,7 +68,7 @@ void awaitStatusUpdated(int timeout) { .untilAsserted( () -> { TestCustomResource cr = - operator.getNamedResource(TestCustomResource.class, + operator.get(TestCustomResource.class, TestUtils.TEST_CUSTOM_RESOURCE_NAME); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index d2954b86ee..eb556535a1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -35,10 +35,8 @@ public void receivingPeriodicEvents() { .pollInterval( EventSourceTestCustomResourceController.TIMER_PERIOD / 2, TimeUnit.MILLISECONDS) .untilAsserted( - () -> { - assertThat(TestUtils.getNumberOfExecutions(operator)) - .isGreaterThanOrEqualTo(4); - }); + () -> assertThat(TestUtils.getNumberOfExecutions(operator)) + .isGreaterThanOrEqualTo(4)); } public EventSourceTestCustomResource createTestCustomResource(String id) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 66b9d73b98..1cb3fa7096 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -70,7 +70,7 @@ private InformerEventSourceTestCustomResource initialCustomResource() { private void waitForCRStatusValue(String value) { await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { var cr = - operator.getNamedResource(InformerEventSourceTestCustomResource.class, RESOURCE_NAME); + operator.get(InformerEventSourceTestCustomResource.class, RESOURCE_NAME); assertThat(cr.getStatus()).isNotNull(); assertThat(cr.getStatus().getConfigMapValue()).isEqualTo(value); }); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index a68ba0bc7f..e5fcde6934 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -52,7 +52,7 @@ public void retryFailedExecution() { .isEqualTo(RetryTestCustomResourceController.NUMBER_FAILED_EXECUTIONS + 1); RetryTestCustomResource finalResource = - operator.getNamedResource(RetryTestCustomResource.class, + operator.get(RetryTestCustomResource.class, resource.getMetadata().getName()); assertThat(finalResource.getStatus().getState()) .isEqualTo(RetryTestCustomResourceStatus.State.SUCCESS); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index c09982b639..1cee81c054 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -99,7 +99,7 @@ void awaitStatusUpdated(String name) { .untilAsserted( () -> { SubResourceTestCustomResource cr = - operator.getNamedResource(SubResourceTestCustomResource.class, name); + operator.get(SubResourceTestCustomResource.class, name); assertThat(cr.getMetadata().getFinalizers()).hasSize(1); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index 7d513ddd33..7d08ca6110 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -36,7 +36,7 @@ public void updatesSubResourceStatus() { DoubleUpdateTestCustomResource customResource = operator - .getNamedResource(DoubleUpdateTestCustomResource.class, + .get(DoubleUpdateTestCustomResource.class, resource.getMetadata().getName()); assertThat(TestUtils.getNumberOfExecutions(operator)) @@ -57,7 +57,7 @@ void awaitStatusUpdated(String name) { .untilAsserted( () -> { DoubleUpdateTestCustomResource cr = - operator.getNamedResource(DoubleUpdateTestCustomResource.class, name); + operator.get(DoubleUpdateTestCustomResource.class, name); assertThat(cr) .isNotNull(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java index e5c44198b4..e5c34e1610 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java @@ -27,7 +27,7 @@ public class InformerEventSourceTestCustomResourceController implements private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomResourceController.class); - public static final String RELATED_RESOURCE_UID = "relatedResourceUID"; + public static final String RELATED_RESOURCE_UID = "relatedResourceName"; public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; private KubernetesClient kubernetesClient; diff --git a/pom.xml b/pom.xml index 470cc27b92..7686a9591f 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 5.8.0 1.7.32 2.14.1 - 3.12.4 + 4.0.0 3.12.0 1.0 0.19 @@ -67,7 +67,7 @@ 2.8.2 2.5.2 5.0.0 - 1.1.1 + 1.2.1 2.16.0 1.0 1.6.2 @@ -177,7 +177,6 @@ https://oss.sonatype.org/content/repositories/snapshots - @@ -363,6 +362,7 @@ release + org.apache.maven.plugins maven-surefire-plugin @@ -431,5 +431,4 @@ - From 5e6b6e2c47c5bae0610f1ad906c31f097d90f9d3 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 12 Oct 2021 14:29:53 +0200 Subject: [PATCH 0098/1608] chore: update version to 2.0.0-SNAPSHOT --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- samples/common/pom.xml | 2 +- samples/pom.xml | 2 +- samples/pure-java/pom.xml | 2 +- samples/spring-boot-plain/pom.xml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index a12c83738b..2866f42257 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index e578810de5..ce8115829a 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 6256cc7de3..5ffef0d184 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 996c29bcae..2fe67f8716 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 7686a9591f..def68a848c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/samples/common/pom.xml b/samples/common/pom.xml index 066f5007b3..5e873dfe23 100644 --- a/samples/common/pom.xml +++ b/samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-samples-common diff --git a/samples/pom.xml b/samples/pom.xml index 73528a4b20..386cc0c160 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT java-operator-sdk-samples diff --git a/samples/pure-java/pom.xml b/samples/pure-java/pom.xml index c53735c45b..74a9cebd56 100644 --- a/samples/pure-java/pom.xml +++ b/samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-samples-pure-java diff --git a/samples/spring-boot-plain/pom.xml b/samples/spring-boot-plain/pom.xml index e361ca1cfc..82fd73013b 100644 --- a/samples/spring-boot-plain/pom.xml +++ b/samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-samples - 1.9.9-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-samples-spring-boot-plain From 6e0100152c2aac0ef526e3da1cb1c76c53a435b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 13 Oct 2021 15:14:42 +0200 Subject: [PATCH 0099/1608] Reschedule delete (#600) * chore: renaming vars named k8sClient to kubernetsClient * chore(deps): bump jandex-maven-plugin from 1.1.1 to 1.2.1 (#592) Bumps [jandex-maven-plugin](https://github.com/wildfly/jandex-maven-plugin) from 1.1.1 to 1.2.1. - [Release notes](https://github.com/wildfly/jandex-maven-plugin/releases) - [Commits](https://github.com/wildfly/jandex-maven-plugin/compare/1.1.1...1.2.1) --- updated-dependencies: - dependency-name: org.jboss.jandex:jandex-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump mockito-core from 3.12.4 to 4.0.0 (#591) Bumps [mockito-core](https://github.com/mockito/mockito) from 3.12.4 to 4.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v3.12.4...v4.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: Build PR on v2 * chore(ci): use Java 17 * chore(ci): use only Temurin distribution * chore: add generics to PostExecutionControl to reduce IDEs noise (#594) * chore: polish the junit5 extension (#593) * feat: Use informers as CustomResourceEventSource backbone and cache This is a major change and backbone of v2. We change also how the Resources are addressed both internally and from event source with CustomResourceID. Additional improvements also added. * feat: delete reschedule * fix: naming after review Co-authored-by: Ioannis Canellos Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chris Laprun Co-authored-by: Luca Burgazzoli --- .../operator/api/BaseControl.java | 22 ++++++++++++++ .../operator/api/DeleteControl.java | 29 +++++++++++++++++-- .../operator/api/ResourceController.java | 8 ++--- .../operator/api/UpdateControl.java | 19 +----------- .../processing/ConfiguredController.java | 11 ++----- .../operator/processing/EventDispatcher.java | 22 +++++++++----- .../operator/api/DeleteControlTest.java | 15 ++++++++++ .../processing/EventDispatcherTest.java | 21 ++++++++++++-- .../simple/TestCustomResourceController.java | 2 +- .../simple/TestCustomResourceController.java | 2 +- .../ControllerImplemented2Interfaces.java | 2 +- ...rImplementedIntermediateAbstractClass.java | 2 +- .../MultilevelController.java | 2 +- .../sample/CustomServiceController.java | 2 +- 14 files changed, 109 insertions(+), 50 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java new file mode 100644 index 0000000000..327bc34791 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.api; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +public abstract class BaseControl { + + private Long scheduleDelay = null; + + public T rescheduleAfter(long delay) { + this.scheduleDelay = delay; + return (T) this; + } + + public T rescheduleAfter(long delay, TimeUnit timeUnit) { + return rescheduleAfter(timeUnit.toMillis(delay)); + } + + public Optional getScheduleDelay() { + return Optional.ofNullable(scheduleDelay); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java index 2c721442cd..b1ddac3df3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java @@ -1,5 +1,30 @@ package io.javaoperatorsdk.operator.api; -public enum DeleteControl { - DEFAULT_DELETE, NO_FINALIZER_REMOVAL +public class DeleteControl extends BaseControl { + + private final boolean removeFinalizer; + + private DeleteControl(boolean removeFinalizer) { + this.removeFinalizer = removeFinalizer; + } + + public static DeleteControl defaultDelete() { + return new DeleteControl(true); + } + + public static DeleteControl noFinalizerRemoval() { + return new DeleteControl(false); + } + + public boolean isRemoveFinalizer() { + return removeFinalizer; + } + + @Override + public DeleteControl rescheduleAfter(long delay) { + if (removeFinalizer == true) { + throw new IllegalStateException("Cannot reschedule deleteResource if removing finalizer"); + } + return super.rescheduleAfter(delay); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java index 6d1adb84b7..edad82be27 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java @@ -9,7 +9,7 @@ public interface ResourceController { * The implementation should delete the associated component(s). Note that this is method is * called when an object is marked for deletion. After it's executed the custom resource finalizer * is automatically removed by the framework; unless the return value is - * {@link DeleteControl#NO_FINALIZER_REMOVAL}, which indicates that the controller has determined + * {@link DeleteControl#noFinalizerRemoval()}, which indicates that the controller has determined * that the resource should not be deleted yet, in which case it is up to the controller to * restore the resource's status so that it's not marked for deletion anymore. * @@ -21,13 +21,13 @@ public interface ResourceController { * * @param resource the resource that is marked for deletion * @param context the context with which the operation is executed - * @return {@link DeleteControl#DEFAULT_DELETE} - so the finalizer is automatically removed after - * the call. {@link DeleteControl#NO_FINALIZER_REMOVAL} if you don't want to remove the + * @return {@link DeleteControl#defaultDelete()} - so the finalizer is automatically removed after + * the call. {@link DeleteControl#noFinalizerRemoval()} if you don't want to remove the * finalizer to indicate that the resource should not be deleted after all, in which case * the controller should restore the resource's state appropriately. */ default DeleteControl deleteResource(R resource, Context context) { - return DeleteControl.DEFAULT_DELETE; + return DeleteControl.defaultDelete(); } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java index d9e14a6c5a..18cc2ca4ee 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java @@ -1,16 +1,12 @@ package io.javaoperatorsdk.operator.api; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - import io.fabric8.kubernetes.client.CustomResource; -public class UpdateControl { +public class UpdateControl extends BaseControl> { private final T customResource; private final boolean updateStatusSubResource; private final boolean updateCustomResource; - private Long reScheduleDelay = null; private UpdateControl( T customResource, boolean updateStatusSubResource, boolean updateCustomResource) { @@ -47,19 +43,6 @@ public static UpdateControl noUpdate() { return new UpdateControl<>(null, false, false); } - public UpdateControl withReSchedule(long delay, TimeUnit timeUnit) { - return withReSchedule(timeUnit.toMillis(delay)); - } - - public UpdateControl withReSchedule(long delay) { - this.reScheduleDelay = delay; - return this; - } - - public Optional getReScheduleDelay() { - return Optional.ofNullable(reScheduleDelay); - } - public T getCustomResource() { return customResource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index 120a05bb65..3ce90ebb67 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -52,15 +52,8 @@ public String controllerName() { } @Override - public String successTypeName(DeleteControl result) { - switch (result) { - case DEFAULT_DELETE: - return "delete"; - case NO_FINALIZER_REMOVAL: - return "finalizerNotRemoved"; - default: - return "unknown"; - } + public String successTypeName(DeleteControl deleteControl) { + return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 92347d5460..0e385dc553 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -8,11 +8,7 @@ import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.DefaultContext; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.EventList; @@ -152,10 +148,16 @@ private PostExecutionControl createPostExecutionControl(R updatedCustomResour } else { postExecutionControl = PostExecutionControl.defaultDispatch(); } - updateControl.getReScheduleDelay().ifPresent(postExecutionControl::withReSchedule); + updatePostExecutionControlWithReschedule(postExecutionControl, updateControl); return postExecutionControl; } + private void updatePostExecutionControlWithReschedule( + PostExecutionControl postExecutionControl, + BaseControl baseControl) { + baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); + } + private PostExecutionControl handleDelete(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", @@ -165,7 +167,9 @@ private PostExecutionControl handleDelete(R resource, Context context) { DeleteControl deleteControl = controller.deleteResource(resource, context); final var useFinalizer = configuration().useFinalizer(); if (useFinalizer) { - if (deleteControl == DeleteControl.DEFAULT_DELETE + // note that we don't reschedule here even if instructed. Removing finalizer means that + // cleanup is finished, nothing left to done + if (deleteControl.isRemoveFinalizer() && resource.hasFinalizer(configuration().getFinalizer())) { R customResource = removeFinalizer(resource); return PostExecutionControl.customResourceUpdated(customResource); @@ -177,7 +181,9 @@ private PostExecutionControl handleDelete(R resource, Context context) { getVersion(resource), deleteControl, useFinalizer); - return PostExecutionControl.defaultDispatch(); + PostExecutionControl postExecutionControl = PostExecutionControl.defaultDispatch(); + updatePostExecutionControlWithReschedule(postExecutionControl, deleteControl); + return postExecutionControl; } private void updateCustomResourceWithFinalizer(R resource) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java new file mode 100644 index 0000000000..645c997285 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.api; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DeleteControlTest { + + @Test + void cannotReScheduleForDefaultDelete() { + Assertions.assertThrows(IllegalStateException.class, () -> { + DeleteControl.defaultDelete().rescheduleAfter(1000L); + }); + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index 2a8ad4b0b9..00693e2f34 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -66,7 +66,7 @@ void setup() { when(controller.createOrUpdateResource(eq(testCustomResource), any())) .thenReturn(UpdateControl.updateCustomResource(testCustomResource)); when(controller.deleteResource(eq(testCustomResource), any())) - .thenReturn(DeleteControl.DEFAULT_DELETE); + .thenReturn(DeleteControl.defaultDelete()); when(customResourceFacade.replaceWithLock(any())).thenReturn(null); } @@ -202,7 +202,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); when(controller.deleteResource(eq(testCustomResource), any())) - .thenReturn(DeleteControl.NO_FINALIZER_REMOVAL); + .thenReturn(DeleteControl.noFinalizerRemoval()); markForDeletion(testCustomResource); eventDispatcher.handleExecution( @@ -297,7 +297,7 @@ void setReScheduleToPostExecutionControlFromUpdateControl() { when(controller.createOrUpdateResource(eq(testCustomResource), any())) .thenReturn( - UpdateControl.updateStatusSubResource(testCustomResource).withReSchedule(1000L)); + UpdateControl.updateStatusSubResource(testCustomResource).rescheduleAfter(1000L)); PostExecutionControl control = eventDispatcher.handleExecution( executionScopeWithCREvent(ADDED, testCustomResource)); @@ -305,6 +305,21 @@ void setReScheduleToPostExecutionControlFromUpdateControl() { assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); } + @Test + void reScheduleOnDeleteWithoutFinalizerRemoval() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + markForDeletion(testCustomResource); + + when(controller.deleteResource(eq(testCustomResource), any())) + .thenReturn( + DeleteControl.noFinalizerRemoval().rescheduleAfter(1000L)); + + PostExecutionControl control = eventDispatcher.handleExecution( + executionScopeWithCREvent(UPDATED, testCustomResource)); + + assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); + } + private void markForDeletion(CustomResource customResource) { customResource.getMetadata().setDeletionTimestamp("2019-8-10"); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java index 0af533d0c3..2cef04a09a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java @@ -57,7 +57,7 @@ public DeleteControl deleteResource( resource.getSpec().getConfigMapName(), resource.getMetadata().getName()); } - return DeleteControl.DEFAULT_DELETE; + return DeleteControl.defaultDelete(); } @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java index 4726a0fdfa..b6aabd1e67 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java @@ -81,7 +81,7 @@ public DeleteControl deleteResource( resource.getSpec().getConfigMapName(), resource.getMetadata().getName()); } - return DeleteControl.DEFAULT_DELETE; + return DeleteControl.defaultDelete(); } @Override diff --git a/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java b/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java index 406d314091..e56943fa30 100644 --- a/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java +++ b/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java @@ -21,6 +21,6 @@ public UpdateControl createOrUpdateResource(MyCustomResource c @Override public DeleteControl deleteResource(MyCustomResource customResource, Context context) { - return DeleteControl.DEFAULT_DELETE; + return DeleteControl.defaultDelete(); } } diff --git a/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java b/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java index f048fb25f0..4f5619b4a4 100644 --- a/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java +++ b/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java @@ -18,6 +18,6 @@ public UpdateControl createOrUpdateResource public DeleteControl deleteResource(AbstractController.MyCustomResource customResource, Context context) { - return DeleteControl.DEFAULT_DELETE; + return DeleteControl.defaultDelete(); } } diff --git a/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java b/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java index 8c1b6a7ffb..d941a16d9d 100644 --- a/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java +++ b/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java @@ -22,7 +22,7 @@ public UpdateControl createOrUpdateResour public DeleteControl deleteResource(MultilevelController.MyCustomResource customResource, Context context) { - return DeleteControl.DEFAULT_DELETE; + return DeleteControl.defaultDelete(); } } diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java index aacff140c9..67fb81dbd7 100644 --- a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java +++ b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java @@ -36,7 +36,7 @@ public CustomServiceController(KubernetesClient kubernetesClient) { @Override public DeleteControl deleteResource(CustomService resource, Context context) { log.info("Execution deleteResource for: {}", resource.getMetadata().getName()); - return DeleteControl.DEFAULT_DELETE; + return DeleteControl.defaultDelete(); } @Override From 8e9300e4fa655e8e582c22093b7b1cfc13f8a5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 13 Oct 2021 15:15:28 +0200 Subject: [PATCH 0100/1608] Refined Interface of `EventSource` and `EventSourceManager` (#597) * WIP * Addressing Custom Resource by Name and Namespace refactor + Informer Cache WIP * fix: DefaultEventHandler init from EventSourceManager * fix: custom resource selector test improvement * fix: wip test imrpovements * fix: test improvements * fix: further improvements * fix: build * feature: add mvn jar to gitignore * Exposing CustomResourceEventSource and informers * fix: cleanup * fix: remove caching optimization since it not possible anymore with informer * fix: formatting * refactor: make name/namespace final * feature: Simple label selector support * fix: formatting * fix: code inspection reports * fix: merge from v2 * fix: removed most deprecated apis * chore: renaming vars named k8sClient to kubernetsClient * chore(deps): bump jandex-maven-plugin from 1.1.1 to 1.2.1 (#592) Bumps [jandex-maven-plugin](https://github.com/wildfly/jandex-maven-plugin) from 1.1.1 to 1.2.1. - [Release notes](https://github.com/wildfly/jandex-maven-plugin/releases) - [Commits](https://github.com/wildfly/jandex-maven-plugin/compare/1.1.1...1.2.1) --- updated-dependencies: - dependency-name: org.jboss.jandex:jandex-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump mockito-core from 3.12.4 to 4.0.0 (#591) Bumps [mockito-core](https://github.com/mockito/mockito) from 3.12.4 to 4.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v3.12.4...v4.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: Build PR on v2 * chore(ci): use Java 17 * chore(ci): use only Temurin distribution * fix: Updated informer mapping to CustomResourceID * chore: add generics to PostExecutionControl to reduce IDEs noise (#594) * chore: polish the junit5 extension (#593) * fix: EventSourceManager API wip * fix: code review fixes * fix: improvements of Event Source related APIs * fix: remarks from code review Co-authored-by: Chris Laprun Co-authored-by: Ioannis Canellos Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Luca Burgazzoli --- .../processing/DefaultEventHandler.java | 2 +- .../event/DefaultEventSourceManager.java | 95 +++++-------------- .../processing/event/EventSource.java | 7 +- .../processing/event/EventSourceManager.java | 21 +--- .../event/internal/TimerEventSource.java | 2 +- .../processing/DefaultEventHandlerTest.java | 2 +- .../event/DefaultEventSourceManagerTest.java | 37 +++----- .../event/internal/TimerEventSourceTest.java | 4 +- ...entSourceTestCustomResourceController.java | 2 +- ...entSourceTestCustomResourceController.java | 2 +- 10 files changed, 50 insertions(+), 124 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 5d440193df..aac6a08289 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -271,7 +271,7 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) } private void cleanupAfterDeletedEvent(CustomResourceID customResourceUid) { - eventSourceManager.cleanup(customResourceUid); + eventSourceManager.cleanupForCustomResource(customResourceUid); eventBuffer.cleanup(customResourceUid); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 8142544c01..ef76244de3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -1,11 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; -import java.io.IOException; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; +import java.util.*; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; @@ -22,24 +17,22 @@ public class DefaultEventSourceManager> implements EventSourceManager { - public static final String RETRY_TIMER_EVENT_SOURCE_NAME = "retry-timer-event-source"; - public static final String CUSTOM_RESOURCE_EVENT_SOURCE_NAME = "custom-resource-event-source"; private static final Logger log = LoggerFactory.getLogger(DefaultEventSourceManager.class); private final ReentrantLock lock = new ReentrantLock(); - private final Map eventSources = new ConcurrentHashMap<>(); + private final Set eventSources = Collections.synchronizedSet(new HashSet<>()); private DefaultEventHandler defaultEventHandler; private TimerEventSource retryTimerEventSource; + private CustomResourceEventSource customResourceEventSource; DefaultEventSourceManager(DefaultEventHandler defaultEventHandler) { init(defaultEventHandler); } public DefaultEventSourceManager(ConfiguredController controller) { - CustomResourceEventSource customResourceEventSource = - new CustomResourceEventSource<>(controller); + customResourceEventSource = new CustomResourceEventSource<>(controller); init(new DefaultEventHandler<>(controller, customResourceEventSource)); - registerEventSource(CUSTOM_RESOURCE_EVENT_SOURCE_NAME, customResourceEventSource); + registerEventSource(customResourceEventSource); } private void init(DefaultEventHandler defaultEventHandler) { @@ -47,29 +40,26 @@ private void init(DefaultEventHandler defaultEventHandler) { defaultEventHandler.setEventSourceManager(this); this.retryTimerEventSource = new TimerEventSource<>(); - registerEventSource(RETRY_TIMER_EVENT_SOURCE_NAME, retryTimerEventSource); + registerEventSource(retryTimerEventSource); } @Override public void close() { + lock.lock(); try { - lock.lock(); - try { defaultEventHandler.close(); } catch (Exception e) { log.warn("Error closing event handler", e); } - - for (var entry : eventSources.entrySet()) { + log.debug("Closing event sources."); + for (var eventSource : eventSources) { try { - log.debug("Closing {} -> {}", entry.getKey(), entry.getValue()); - entry.getValue().close(); + eventSource.close(); } catch (Exception e) { - log.warn("Error closing {} -> {}", entry.getKey(), entry.getValue(), e); + log.warn("Error closing {} -> {}", eventSource); } } - eventSources.clear(); } finally { lock.unlock(); @@ -77,17 +67,12 @@ public void close() { } @Override - public final void registerEventSource(String name, EventSource eventSource) + public final void registerEventSource(EventSource eventSource) throws OperatorException { Objects.requireNonNull(eventSource, "EventSource must not be null"); - + lock.lock(); try { - lock.lock(); - if (eventSources.containsKey(name)) { - throw new IllegalStateException( - "Event source with name already registered. Event source name: " + name); - } - eventSources.put(name, eventSource); + eventSources.add(eventSource); eventSource.setEventHandler(defaultEventHandler); eventSource.start(); } catch (Throwable e) { @@ -95,46 +80,18 @@ public final void registerEventSource(String name, EventSource eventSource) // leave untouched throw e; } - throw new OperatorException("Couldn't register event source named '" + name + "'", e); - } finally { - lock.unlock(); - } - } - - @Override - public Optional deRegisterEventSource(String name) { - try { - lock.lock(); - EventSource currentEventSource = eventSources.remove(name); - if (currentEventSource != null) { - try { - currentEventSource.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - return Optional.ofNullable(currentEventSource); + throw new OperatorException( + "Couldn't register event source: " + eventSource.getClass().getName(), e); } finally { lock.unlock(); } } - @Override - public Optional deRegisterCustomResourceFromEventSource( - String eventSourceName, CustomResourceID customResourceUid) { + public void cleanupForCustomResource(CustomResourceID customResourceUid) { + lock.lock(); try { - lock.lock(); - EventSource eventSource = this.eventSources.get(eventSourceName); - if (eventSource == null) { - log.warn( - "Event producer: {} not found for custom resource: {}", - eventSourceName, - customResourceUid); - return Optional.empty(); - } else { - eventSource.eventSourceDeRegisteredForResource(customResourceUid); - return Optional.of(eventSource); + for (EventSource eventSource : this.eventSources) { + eventSource.cleanupForCustomResource(customResourceUid); } } finally { lock.unlock(); @@ -146,19 +103,13 @@ public TimerEventSource getRetryTimerEventSource() { } @Override - public Map getRegisteredEventSources() { - return Collections.unmodifiableMap(eventSources); + public Set getRegisteredEventSources() { + return Collections.unmodifiableSet(eventSources); } @Override public CustomResourceEventSource getCustomResourceEventSource() { - return (CustomResourceEventSource) getRegisteredEventSources() - .get(CUSTOM_RESOURCE_EVENT_SOURCE_NAME); + return customResourceEventSource; } - public void cleanup(CustomResourceID customResourceUid) { - getRegisteredEventSources() - .keySet() - .forEach(k -> deRegisterCustomResourceFromEventSource(k, customResourceUid)); - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java index 1cc05f632c..22187ddb86 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java @@ -20,5 +20,10 @@ default void close() throws IOException {} void setEventHandler(EventHandler eventHandler); - default void eventSourceDeRegisteredForResource(CustomResourceID customResourceUid) {} + /** + * Automatically called when a custom resource is deleted from the cluster. + * + * @param customResourceUid - id of custom resource + */ + default void cleanupForCustomResource(CustomResourceID customResourceUid) {} } 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 86638c2786..7e049e7c51 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 @@ -2,8 +2,7 @@ import java.io.Closeable; import java.io.IOException; -import java.util.Map; -import java.util.Optional; +import java.util.Set; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.OperatorException; @@ -14,29 +13,15 @@ public interface EventSourceManager> extends Clos /** * Add the {@link EventSource} identified by the given name to the event manager. * - * @param name the name of the {@link EventSource} to add * @param eventSource the {@link EventSource} to register * @throws IllegalStateException if an {@link EventSource} with the same name is already * registered. * @throws OperatorException if an error occurred during the registration process */ - void registerEventSource(String name, EventSource eventSource) + void registerEventSource(EventSource eventSource) throws IllegalStateException, OperatorException; - /** - * Remove the {@link EventSource} identified by the given name from the event - * manager. - * - * @param name the name of the {@link EventSource} to remove - * @return an optional {@link EventSource} which would be empty if no {@link EventSource} have - * been registered with the given name. - */ - Optional deRegisterEventSource(String name); - - Optional deRegisterCustomResourceFromEventSource( - String name, CustomResourceID customResourceUid); - - Map getRegisteredEventSources(); + Set getRegisteredEventSources(); CustomResourceEventSource getCustomResourceEventSource(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index 51f21fc4d4..d4638f5c3b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -50,7 +50,7 @@ public void scheduleOnce(R customResource, long delay) { } @Override - public void eventSourceDeRegisteredForResource(CustomResourceID customResourceUid) { + public void cleanupForCustomResource(CustomResourceID customResourceUid) { cancelSchedule(customResourceUid); cancelOnceSchedule(customResourceUid); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 620a7c9ac7..023d589e03 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -119,7 +119,7 @@ public void cleanUpAfterDeleteEvent() { waitMinimalTime(); verify(defaultEventSourceManagerMock, times(1)) - .cleanup(CustomResourceID.fromResource(customResource)); + .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java index 089215486e..ad87000a52 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; import java.io.IOException; -import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -10,7 +10,6 @@ import io.javaoperatorsdk.operator.processing.DefaultEventHandler; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -18,8 +17,6 @@ class DefaultEventSourceManagerTest { - public static final String CUSTOM_EVENT_SOURCE_NAME = "CustomEventSource"; - private DefaultEventHandler defaultEventHandlerMock = mock(DefaultEventHandler.class); private DefaultEventSourceManager defaultEventSourceManager = new DefaultEventSourceManager(defaultEventHandlerMock); @@ -28,12 +25,12 @@ class DefaultEventSourceManagerTest { public void registersEventSource() { EventSource eventSource = mock(EventSource.class); - defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, eventSource); + defaultEventSourceManager.registerEventSource(eventSource); - Map registeredSources = + Set registeredSources = defaultEventSourceManager.getRegisteredEventSources(); - assertThat(registeredSources.entrySet()).hasSize(2); - assertThat(registeredSources.get(CUSTOM_EVENT_SOURCE_NAME)).isEqualTo(eventSource); + assertThat(registeredSources).hasSize(2); + verify(eventSource, times(1)).setEventHandler(eq(defaultEventHandlerMock)); verify(eventSource, times(1)).start(); } @@ -42,8 +39,8 @@ public void registersEventSource() { public void closeShouldCascadeToEventSources() throws IOException { EventSource eventSource = mock(EventSource.class); EventSource eventSource2 = mock(EventSource.class); - defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, eventSource); - defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME + "2", eventSource2); + defaultEventSourceManager.registerEventSource(eventSource); + defaultEventSourceManager.registerEventSource(eventSource2); defaultEventSourceManager.close(); @@ -51,28 +48,16 @@ public void closeShouldCascadeToEventSources() throws IOException { verify(eventSource2, times(1)).close(); } - @Test - public void throwExceptionIfRegisteringEventSourceWithSameName() { - EventSource eventSource = mock(EventSource.class); - EventSource eventSource2 = mock(EventSource.class); - - defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, eventSource); - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy( - () -> defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, - eventSource2)); - } - @Test public void deRegistersEventSources() { CustomResource customResource = TestUtils.testCustomResource(); EventSource eventSource = mock(EventSource.class); - defaultEventSourceManager.registerEventSource(CUSTOM_EVENT_SOURCE_NAME, eventSource); + defaultEventSourceManager.registerEventSource(eventSource); - defaultEventSourceManager.deRegisterCustomResourceFromEventSource( - CUSTOM_EVENT_SOURCE_NAME, CustomResourceID.fromResource(customResource)); + defaultEventSourceManager + .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); verify(eventSource, times(1)) - .eventSourceDeRegisteredForResource(eq(CustomResourceID.fromResource(customResource))); + .cleanupForCustomResource(eq(CustomResourceID.fromResource(customResource))); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index f7f2814255..0d9c3b5a11 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -61,7 +61,7 @@ public void deRegistersPeriodicalEventSources() { untilAsserted(() -> assertThat(eventHandlerMock.events).hasSizeGreaterThan(1)); timerEventSource - .eventSourceDeRegisteredForResource(CustomResourceID.fromResource(customResource)); + .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); int size = eventHandlerMock.events.size(); untilAsserted(() -> assertThat(eventHandlerMock.events).hasSize(size)); @@ -103,7 +103,7 @@ public void deRegistersOnceEventSources() { timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource - .eventSourceDeRegisteredForResource(CustomResourceID.fromResource(customResource)); + .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java index f262440cae..27b651c9fb 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java @@ -32,7 +32,7 @@ public class EventSourceTestCustomResourceController @Override public void init(EventSourceManager eventSourceManager) { - eventSourceManager.registerEventSource("Timer", timerEventSource); + eventSourceManager.registerEventSource(timerEventSource); } @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java index e5c34e1610..a2284adfa1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java @@ -37,7 +37,7 @@ public class InformerEventSourceTestCustomResourceController implements public void init(EventSourceManager eventSourceManager) { eventSource = new InformerEventSource<>(kubernetesClient, ConfigMap.class, Mappers.fromAnnotation(RELATED_RESOURCE_UID)); - eventSourceManager.registerEventSource("configmap", eventSource); + eventSourceManager.registerEventSource(eventSource); } @Override From 1dc199397c94dd14b53a23cd702069ef909e0bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 13 Oct 2021 16:54:21 +0200 Subject: [PATCH 0101/1608] Removing events from context (#596) * fix: WIP * fix: Addressing Custom Resource by Name and Namespace refactor + Informer Cache WIP * fix: Build is fixed, (test failing) * fix: Test fixes * fix: minor update * fix: EventSourceManager small fix * fix: Unit tests fixed * fix: DefaultEventHandler init from EventSourceManager * fix: custom resource selector test improvement * fix: wip test imrpovements * fix: test improvements * fix: further improvements * fix: build * feature: add mvn jar to gitignore * Exposing CustomResourceEventSource and informers * fix: cleanup * fix: remove caching optimization since it not possible anymore with informer * fix: formatting * refactor: make name/namespace final * feature: Simple label selector support * fix: formatting * fix: code inspection reports * fix: merge from v2 * fix: removed most deprecated apis * wip: started to remove events from variouse layers * fix: progress with implementation and tests * fix: Updated informer mapping to CustomResourceID * fix: imports * fix: decorational changes * fix: event marker unit test * Default Event Handler Unit tests * fix: fixes after merge * fix: changes from code review * fix: method naming * Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java Co-authored-by: Chris Laprun * Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java Co-authored-by: Chris Laprun * fix: comment * fix: fixes from merge * fix: remove not used method * fix: formatting Co-authored-by: Chris Laprun Co-authored-by: Chris Laprun --- .../javaoperatorsdk/operator/api/Context.java | 4 +- .../operator/api/DefaultContext.java | 10 +- .../operator/api/ResourceController.java | 7 +- .../processing/DefaultEventHandler.java | 114 +++++++++++------- .../operator/processing/EventBuffer.java | 45 ------- .../operator/processing/EventDispatcher.java | 15 +-- .../operator/processing/EventMarker.java | 84 +++++++++++++ .../operator/processing/ExecutionScope.java | 13 +- .../processing/event/DefaultEvent.java | 3 +- .../operator/processing/event/EventList.java | 27 ----- .../event/internal/CustomResourceEvent.java | 13 +- .../internal/CustomResourceEventSource.java | 3 +- .../event/internal/InformerEvent.java | 35 ------ .../event/internal/InformerEventSource.java | 11 +- .../processing/event/internal/TimerEvent.java | 11 -- .../event/internal/TimerEventSource.java | 3 +- .../processing/DefaultEventHandlerTest.java | 84 +++++-------- .../operator/processing/EventBufferTest.java | 60 --------- .../processing/EventDispatcherTest.java | 7 +- .../operator/processing/EventMarkerTest.java | 67 ++++++++++ .../processing/event/EventListTest.java | 24 ---- ...entSourceTestCustomResourceController.java | 1 - 22 files changed, 278 insertions(+), 363 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventList.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEvent.java delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java index 963eb16f76..cc832a2e21 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java @@ -3,11 +3,9 @@ import java.util.Optional; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.EventList; public interface Context { - EventList getEvents(); - Optional getRetryInfo(); + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java index 4614b562df..f32a784e11 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java @@ -3,21 +3,13 @@ import java.util.Optional; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.EventList; public class DefaultContext implements Context { private final RetryInfo retryInfo; - private final EventList events; - public DefaultContext(EventList events, RetryInfo retryInfo) { + public DefaultContext(RetryInfo retryInfo) { this.retryInfo = retryInfo; - this.events = events; - } - - @Override - public EventList getEvents() { - return events; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java index edad82be27..5779cc4eb9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java @@ -6,12 +6,15 @@ public interface ResourceController { /** + * Note that this method is used in combination of finalizers. If automatic finalizer handling is + * turned off for the controller, this method is not called. + * * The implementation should delete the associated component(s). Note that this is method is * called when an object is marked for deletion. After it's executed the custom resource finalizer * is automatically removed by the framework; unless the return value is * {@link DeleteControl#noFinalizerRemoval()}, which indicates that the controller has determined - * that the resource should not be deleted yet, in which case it is up to the controller to - * restore the resource's status so that it's not marked for deletion anymore. + * that the resource should not be deleted yet. This is usually a corner case, when a cleanup is + * tried again eventually. * *

* It's important that this method be idempotent, as it could be called several times, depending diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index aac6a08289..608a47567b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -20,11 +20,12 @@ import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.processing.retry.Retry; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; -import static io.javaoperatorsdk.operator.EventListUtils.containsCustomResourceDeletedEvent; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; /** @@ -38,7 +39,6 @@ public class DefaultEventHandler> implements Even @Deprecated private static EventMonitor monitor = EventMonitor.NOOP; - private final EventBuffer eventBuffer; private final Set underProcessing = new HashSet<>(); private final EventDispatcher eventDispatcher; private final Retry retry; @@ -50,6 +50,7 @@ public class DefaultEventHandler> implements Even private volatile boolean running; private final ResourceCache resourceCache; private DefaultEventSourceManager eventSourceManager; + private final EventMarker eventMarker; public DefaultEventHandler(ConfiguredController controller, ResourceCache resourceCache) { this( @@ -58,18 +59,20 @@ public DefaultEventHandler(ConfiguredController controller, ResourceCache controller.getConfiguration().getName(), new EventDispatcher<>(controller), GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), - controller.getConfiguration().getConfigurationService().getMetrics().getEventMonitor()); + controller.getConfiguration().getConfigurationService().getMetrics().getEventMonitor(), + new EventMarker()); } DefaultEventHandler(EventDispatcher eventDispatcher, ResourceCache resourceCache, String relatedControllerName, - Retry retry) { - this(resourceCache, null, relatedControllerName, eventDispatcher, retry, null); + Retry retry, EventMarker eventMarker) { + this(resourceCache, null, relatedControllerName, eventDispatcher, retry, null, eventMarker); } private DefaultEventHandler(ResourceCache resourceCache, ExecutorService executor, String relatedControllerName, - EventDispatcher eventDispatcher, Retry retry, EventMonitor monitor) { + EventDispatcher eventDispatcher, Retry retry, EventMonitor monitor, + EventMarker eventMarker) { this.running = true; this.executor = executor == null @@ -79,9 +82,9 @@ private DefaultEventHandler(ResourceCache resourceCache, ExecutorService exec this.controllerName = relatedControllerName; this.eventDispatcher = eventDispatcher; this.retry = retry; - eventBuffer = new EventBuffer(); this.resourceCache = resourceCache; this.eventMonitor = monitor != null ? monitor : EventMonitor.NOOP; + this.eventMarker = eventMarker; } public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { @@ -113,71 +116,75 @@ private EventMonitor monitor() { @Override public void handleEvent(Event event) { + lock.lock(); try { - lock.lock(); log.debug("Received event: {}", event); if (!this.running) { log.debug("Skipping event: {} because the event handler is shutting down", event); return; } final var monitor = monitor(); - eventBuffer.addEvent(event.getRelatedCustomResourceID(), event); monitor.processedEvent(event.getRelatedCustomResourceID(), event); - executeBufferedEvents(event.getRelatedCustomResourceID()); - } finally { - lock.unlock(); - } - } - @Override - public void close() { - try { - lock.lock(); - this.running = false; + handleEventMarking(event); + if (!eventMarker.deleteEventPresent(event.getRelatedCustomResourceID())) { + submitReconciliationExecution(event.getRelatedCustomResourceID()); + } else { + cleanupForDeletedEvent(event.getRelatedCustomResourceID()); + } } finally { lock.unlock(); } } - private boolean executeBufferedEvents(CustomResourceID customResourceUid) { - boolean newEventForResourceId = eventBuffer.containsEvents(customResourceUid); + private boolean submitReconciliationExecution(CustomResourceID customResourceUid) { boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); Optional latestCustomResource = resourceCache.getCustomResource(customResourceUid); - if (!controllerUnderExecution && newEventForResourceId && latestCustomResource.isPresent()) { + if (!controllerUnderExecution + && latestCustomResource.isPresent()) { setUnderExecutionProcessing(customResourceUid); ExecutionScope executionScope = new ExecutionScope( - eventBuffer.getAndRemoveEventsForExecution(customResourceUid), latestCustomResource.get(), retryInfo(customResourceUid)); + eventMarker.unMarkEventReceived(customResourceUid); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ControllerExecution(executionScope)); return true; } else { log.debug( - "Skipping executing controller for resource id: {}. Events in queue: {}." + "Skipping executing controller for resource id: {}." + " Controller in execution: {}. Latest CustomResource present: {}", customResourceUid, - newEventForResourceId, controllerUnderExecution, latestCustomResource.isPresent()); if (latestCustomResource.isEmpty()) { - log.warn("no custom resource found in cache for CustomResourceID: {}", customResourceUid); + log.warn("no custom resource found in cache for CustomResourceID: {}", + customResourceUid); } return false; } } + private void handleEventMarking(Event event) { + if (event instanceof CustomResourceEvent && + ((CustomResourceEvent) event).getAction() == ResourceAction.DELETED) { + eventMarker.markDeleteEventReceived(event); + } else if (!eventMarker.deleteEventPresent(event.getRelatedCustomResourceID())) { + eventMarker.markEventReceived(event); + } + } + private RetryInfo retryInfo(CustomResourceID customResourceUid) { return retryState.get(customResourceUid); } void eventProcessingFinished( ExecutionScope executionScope, PostExecutionControl postExecutionControl) { + lock.lock(); try { - lock.lock(); if (!running) { return; } @@ -188,23 +195,29 @@ void eventProcessingFinished( postExecutionControl); unsetUnderExecution(executionScope.getCustomResourceID()); - if (retry != null && postExecutionControl.exceptionDuringExecution()) { + // If a delete event present at this phase, it was received during reconciliation. + // So we either removed the finalizer during reconciliation or we don't use finalizers. + // Either way we don't want to retry. + if (retry != null && postExecutionControl.exceptionDuringExecution() && + !eventMarker.deleteEventPresent(executionScope.getCustomResourceID())) { handleRetryOnException(executionScope); - final var monitor = monitor(); - executionScope.getEvents() - .forEach(e -> monitor.failedEvent(executionScope.getCustomResourceID(), e)); + // todo revisit monitoring since events are not present anymore + // final var monitor = monitor(); executionScope.getEvents().forEach(e -> + // monitor.failedEvent(executionScope.getCustomResourceID(), e)); return; } if (retry != null) { - markSuccessfulExecutionRegardingRetry(executionScope); + handleSuccessfulExecutionRegardingRetry(executionScope); } - if (containsCustomResourceDeletedEvent(executionScope.getEvents())) { - cleanupAfterDeletedEvent(executionScope.getCustomResourceID()); + if (eventMarker.deleteEventPresent(executionScope.getCustomResourceID())) { + cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { - var executed = executeBufferedEvents(executionScope.getCustomResourceID()); - if (!executed) { - reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getCustomResource()); + if (eventMarker.eventPresent(executionScope.getCustomResourceID())) { + submitReconciliationExecution(executionScope.getCustomResourceID()); + } else { + reScheduleExecutionIfInstructed(postExecutionControl, + executionScope.getCustomResource()); } } } finally { @@ -227,13 +240,13 @@ private void reScheduleExecutionIfInstructed(PostExecutionControl postExecuti private void handleRetryOnException(ExecutionScope executionScope) { RetryExecution execution = getOrInitRetryExecution(executionScope); var customResourceID = executionScope.getCustomResourceID(); - boolean newEventsExists = eventBuffer - .newEventsExists(customResourceID); - eventBuffer.putBackEvents(customResourceID, executionScope.getEvents()); + boolean eventPresent = eventMarker.eventPresent(customResourceID); + eventMarker.markEventReceived(customResourceID); - if (newEventsExists) { - log.debug("New events exists for for resource id: {}", customResourceID); - executeBufferedEvents(customResourceID); + if (eventPresent) { + log.debug("New events exists for for resource id: {}", + customResourceID); + submitReconciliationExecution(customResourceID); return; } Optional nextDelay = execution.nextDelay(); @@ -251,7 +264,7 @@ private void handleRetryOnException(ExecutionScope executionScope) { () -> log.error("Exhausted retries for {}", executionScope)); } - private void markSuccessfulExecutionRegardingRetry(ExecutionScope executionScope) { + private void handleSuccessfulExecutionRegardingRetry(ExecutionScope executionScope) { log.debug( "Marking successful execution for resource: {}", getName(executionScope.getCustomResource())); @@ -270,9 +283,9 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) return retryExecution; } - private void cleanupAfterDeletedEvent(CustomResourceID customResourceUid) { + private void cleanupForDeletedEvent(CustomResourceID customResourceUid) { eventSourceManager.cleanupForCustomResource(customResourceUid); - eventBuffer.cleanup(customResourceUid); + eventMarker.cleanup(customResourceUid); } private boolean isControllerUnderExecution(CustomResourceID customResourceUid) { @@ -287,6 +300,15 @@ private void unsetUnderExecution(CustomResourceID customResourceUid) { underProcessing.remove(customResourceUid); } + @Override + public void close() { + lock.lock(); + try { + this.running = false; + } finally { + lock.unlock(); + } + } private class ControllerExecution implements Runnable { private final ExecutionScope executionScope; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java deleted file mode 100644 index b9c565a001..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventBuffer.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.javaoperatorsdk.operator.processing; - -import java.util.*; - -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.Event; - -class EventBuffer { - - private final Map> events = new HashMap<>(); - - public void addEvent(Event event) { - addEvent(event.getRelatedCustomResourceID(), event); - } - - public void addEvent(CustomResourceID uid, Event event) { - Objects.requireNonNull(uid, "uid"); - Objects.requireNonNull(event, "event"); - - List crEvents = events.computeIfAbsent(uid, (customResourceID) -> new LinkedList<>()); - crEvents.add(event); - } - - public boolean newEventsExists(CustomResourceID resourceId) { - return events.get(resourceId) != null && !events.get(resourceId).isEmpty(); - } - - public void putBackEvents(CustomResourceID resourceUid, List oldEvents) { - List crEvents = events.computeIfAbsent(resourceUid, (id) -> new LinkedList<>()); - crEvents.addAll(0, oldEvents); - } - - public boolean containsEvents(CustomResourceID customResourceId) { - return events.get(customResourceId) != null; - } - - public List getAndRemoveEventsForExecution(CustomResourceID resourceUid) { - List crEvents = events.remove(resourceUid); - return crEvents == null ? Collections.emptyList() : crEvents; - } - - public void cleanup(CustomResourceID resourceUid) { - events.remove(resourceUid); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 0e385dc553..dd8ae8dbae 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -10,9 +10,7 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.event.EventList; -import static io.javaoperatorsdk.operator.EventListUtils.containsCustomResourceDeletedEvent; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; @@ -55,15 +53,7 @@ public PostExecutionControl handleExecution(ExecutionScope executionScope) private PostExecutionControl handleDispatch(ExecutionScope executionScope) { R resource = executionScope.getCustomResource(); - log.debug("Handling events: {} for resource {}", executionScope.getEvents(), getName(resource)); - - if (containsCustomResourceDeletedEvent(executionScope.getEvents())) { - log.debug( - "Skipping dispatch processing because of a Delete event: {} with version: {}", - getName(resource), - getVersion(resource)); - return PostExecutionControl.defaultDispatch(); - } + log.debug("Handling dispatch for resource {}", getName(resource)); final var markedForDeletion = resource.isMarkedForDeletion(); if (markedForDeletion && shouldNotDispatchToDelete(resource)) { @@ -75,8 +65,7 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) } Context context = - new DefaultContext<>( - new EventList(executionScope.getEvents()), executionScope.getRetryInfo()); + new DefaultContext<>(executionScope.getRetryInfo()); if (markedForDeletion) { return handleDelete(resource, context); } else { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java new file mode 100644 index 0000000000..9be023416b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java @@ -0,0 +1,84 @@ +package io.javaoperatorsdk.operator.processing; + +import java.util.HashMap; + +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.Event; + +/** + * Manages the state of received events. Basically there can be only three distinct states relevant + * for event processing. Either an event is received, so we eventually process or no event for + * processing at the moment. The third case is if a DELETE event is received, this is a special case + * meaning that the custom resource is deleted. We don't want to do any processing anymore so other + * events are irrelevant for us from this point. Note that the dependant resources are either + * cleaned up by K8S garbage collection or by the controller implementation for cleanup. + */ +public class EventMarker { + + public enum EventingState { + /** Event but NOT Delete event present */ + EVENT_PRESENT, NO_EVENT_PRESENT, + /** Delete event present, from this point other events are not relevant */ + DELETE_EVENT_PRESENT, + } + + private final HashMap eventingState = new HashMap<>(); + + private EventingState getEventingState(CustomResourceID customResourceID) { + EventingState actualState = eventingState.get(customResourceID); + return actualState == null ? EventingState.NO_EVENT_PRESENT : actualState; + } + + private void setEventingState(CustomResourceID customResourceID, EventingState state) { + eventingState.put(customResourceID, state); + } + + public void markEventReceived(Event event) { + markEventReceived(event.getRelatedCustomResourceID()); + } + + public void markEventReceived(CustomResourceID customResourceID) { + if (deleteEventPresent(customResourceID)) { + throw new IllegalStateException("Cannot receive event after a delete event received"); + } + setEventingState(customResourceID, EventingState.EVENT_PRESENT); + } + + public void unMarkEventReceived(CustomResourceID customResourceID) { + var actualState = getEventingState(customResourceID); + switch (actualState) { + case EVENT_PRESENT: + setEventingState(customResourceID, + EventingState.NO_EVENT_PRESENT); + break; + case DELETE_EVENT_PRESENT: + throw new IllegalStateException("Cannot unmark delete event."); + } + } + + public void markDeleteEventReceived(Event event) { + markDeleteEventReceived(event.getRelatedCustomResourceID()); + } + + public void markDeleteEventReceived(CustomResourceID customResourceID) { + setEventingState(customResourceID, EventingState.DELETE_EVENT_PRESENT); + } + + public boolean deleteEventPresent(CustomResourceID customResourceID) { + return getEventingState(customResourceID) == EventingState.DELETE_EVENT_PRESENT; + } + + public boolean eventPresent(CustomResourceID customResourceID) { + var actualState = getEventingState(customResourceID); + return actualState == EventingState.EVENT_PRESENT; + } + + public boolean noEventPresent(CustomResourceID customResourceID) { + var actualState = getEventingState(customResourceID); + return actualState == EventingState.NO_EVENT_PRESENT; + } + + public void cleanup(CustomResourceID customResourceID) { + eventingState.remove(customResourceID); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java index 4f5f0ca7f7..6cf05e9308 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java @@ -1,29 +1,20 @@ package io.javaoperatorsdk.operator.processing; -import java.util.List; - import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.Event; public class ExecutionScope> { - private final List events; // the latest custom resource from cache private final R customResource; private final RetryInfo retryInfo; - public ExecutionScope(List list, R customResource, RetryInfo retryInfo) { - this.events = list; + public ExecutionScope(R customResource, RetryInfo retryInfo) { this.customResource = customResource; this.retryInfo = retryInfo; } - public List getEvents() { - return events; - } - public R getCustomResource() { return customResource; } @@ -35,8 +26,6 @@ public CustomResourceID getCustomResourceID() { @Override public String toString() { return "ExecutionScope{" - + "events=" - + events + ", customResource uid: " + customResource.getMetadata().getUid() + ", version: " diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java index c445f2bf27..7c55939da5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java @@ -2,14 +2,13 @@ @SuppressWarnings("rawtypes") public class DefaultEvent implements Event { - private final CustomResourceID relatedCustomResource; + private final CustomResourceID relatedCustomResource; public DefaultEvent(CustomResourceID targetCustomResource) { this.relatedCustomResource = targetCustomResource; } - @Override public CustomResourceID getRelatedCustomResourceID() { return relatedCustomResource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventList.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventList.java deleted file mode 100644 index d9560f6f1c..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventList.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event; - -import java.util.List; -import java.util.Optional; - -public class EventList { - - private final List eventList; - - public EventList(List eventList) { - this.eventList = eventList; - } - - public List getList() { - return eventList; - } - - public Optional getLatestOfType(Class eventType) { - for (int i = eventList.size() - 1; i >= 0; i--) { - Event event = eventList.get(i); - if (event.getClass().isAssignableFrom(eventType)) { - return (Optional) Optional.of(event); - } - } - return Optional.empty(); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java index 15a67108db..0c20369e0a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java @@ -1,19 +1,15 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.DefaultEvent; public class CustomResourceEvent extends DefaultEvent { private final ResourceAction action; - private final CustomResource customResource; - public CustomResourceEvent(ResourceAction action, - CustomResource resource) { - super(CustomResourceID.fromResource(resource)); - this.customResource = resource; + CustomResourceID customResourceID) { + super(customResourceID); this.action = action; } @@ -21,14 +17,9 @@ public CustomResourceEvent(ResourceAction action, public String toString() { return "CustomResourceEvent{" + "action=" + action + - ", customResource=" + customResource + '}'; } - public CustomResource getCustomResource() { - return customResource; - } - public ResourceAction getAction() { return action; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 9964e7b450..3b56e5b685 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -112,7 +112,8 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource CustomResourceEventFilters.generationAware())); if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { - eventHandler.handleEvent(new CustomResourceEvent(action, clone(customResource))); + eventHandler.handleEvent( + new CustomResourceEvent(action, CustomResourceID.fromResource(customResource))); } else { log.debug( "Skipping event handling resource {} with version: {}", diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java deleted file mode 100644 index 81c78d31b4..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEvent.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.internal; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEvent; - -public class InformerEvent extends DefaultEvent { - - private final ResourceAction action; - private final T resource; - private final T oldResource; - - public InformerEvent(ResourceAction action, - T resource, T oldResource) { - super(CustomResourceID.fromResource(resource)); - this.action = action; - this.resource = resource; - this.oldResource = oldResource; - } - - public T getResource() { - return resource; - } - - public Optional getOldResource() { - return Optional.ofNullable(oldResource); - } - - public ResourceAction getAction() { - return action; - } - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 34e409fa17..42b1c94084 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -13,6 +13,7 @@ import io.fabric8.kubernetes.client.informers.cache.Store; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.DefaultEvent; public class InformerEventSource extends AbstractEventSource { @@ -55,7 +56,7 @@ public InformerEventSource(SharedInformer sharedInformer, sharedInformer.addEventHandler(new ResourceEventHandler<>() { @Override public void onAdd(T t) { - propagateEvent(ResourceAction.ADDED, t, null); + propagateEvent(t); } @Override @@ -65,23 +66,23 @@ public void onUpdate(T oldObject, T newObject) { .equals(newObject.getMetadata().getResourceVersion())) { return; } - propagateEvent(ResourceAction.UPDATED, newObject, oldObject); + propagateEvent(newObject); } @Override public void onDelete(T t, boolean b) { - propagateEvent(ResourceAction.DELETED, t, null); + propagateEvent(t); } }); } - private void propagateEvent(ResourceAction action, T object, T oldObject) { + private void propagateEvent(T object) { var uids = resourceToUIDs.apply(object); if (uids.isEmpty()) { return; } uids.forEach(uid -> { - InformerEvent event = new InformerEvent(action, object, oldObject); + DefaultEvent event = new DefaultEvent(CustomResourceID.fromResource(object)); this.eventHandler.handleEvent(event); }); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEvent.java deleted file mode 100644 index 9e9bfb5040..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.internal; - -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEvent; - -public class TimerEvent extends DefaultEvent { - - public TimerEvent(CustomResourceID relatedCustomResourceUid) { - super(relatedCustomResourceUid); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index d4638f5c3b..5015df4281 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -13,6 +13,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.DefaultEvent; public class TimerEventSource> extends AbstractEventSource { private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class); @@ -94,7 +95,7 @@ public EventProducerTimeTask(CustomResourceID customResourceUid) { public void run() { if (running.get()) { log.debug("Producing event for custom resource id: {}", customResourceUid); - eventHandler.handleEvent(new TimerEvent(customResourceUid)); + eventHandler.handleEvent(new DefaultEvent(customResourceUid)); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 023d589e03..1ecc551951 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -13,11 +13,11 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.DefaultEvent; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -26,13 +26,7 @@ import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.DELETED; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class DefaultEventHandlerTest { @@ -41,6 +35,7 @@ class DefaultEventHandlerTest { public static final int FAKE_CONTROLLER_EXECUTION_DURATION = 250; public static final int SEPARATE_EXECUTION_TIMEOUT = 450; public static final String TEST_NAMESPACE = "default-event-handler-test"; + private EventMarker eventMarker = new EventMarker(); private EventDispatcher eventDispatcherMock = mock(EventDispatcher.class); private DefaultEventSourceManager defaultEventSourceManagerMock = mock(DefaultEventSourceManager.class); @@ -49,11 +44,11 @@ class DefaultEventHandlerTest { private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private DefaultEventHandler defaultEventHandler = - new DefaultEventHandler(eventDispatcherMock, resourceCache, "Test", null); + new DefaultEventHandler(eventDispatcherMock, resourceCache, "Test", null, eventMarker); private DefaultEventHandler defaultEventHandlerWithRetry = new DefaultEventHandler(eventDispatcherMock, resourceCache, "Test", - GenericRetry.defaultLimitedExponentialRetry()); + GenericRetry.defaultLimitedExponentialRetry(), eventMarker); @BeforeEach public void setup() { @@ -91,43 +86,11 @@ public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedExcep .handleExecution(any()); } - @Test - public void buffersAllIncomingEventsWhileControllerInExecution() { - CustomResourceID resourceUid = eventAlreadyUnderProcessing(); - - defaultEventHandler.handleEvent(nonCREvent(resourceUid)); - defaultEventHandler.handleEvent(prepareCREvent(resourceUid)); - - ArgumentCaptor captor = ArgumentCaptor.forClass(ExecutionScope.class); - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) - .handleExecution(captor.capture()); - List events = captor.getAllValues().get(1).getEvents(); - assertThat(events).hasSize(2); - assertThat(events.get(0)).isInstanceOf(TimerEvent.class); - assertThat(events.get(1)).isInstanceOf(CustomResourceEvent.class); - } - - @Test - public void cleanUpAfterDeleteEvent() { - TestCustomResource customResource = testCustomResource(); - when(resourceCache.getCustomResource(CustomResourceID.fromResource(customResource))) - .thenReturn(Optional.of(customResource)); - CustomResourceEvent event = - new CustomResourceEvent(DELETED, customResource); - - defaultEventHandler.handleEvent(event); - - waitMinimalTime(); - verify(defaultEventSourceManagerMock, times(1)) - .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); - } - @Test public void schedulesAnEventRetryOnException() { - Event event = prepareCREvent(); TestCustomResource customResource = testCustomResource(); - ExecutionScope executionScope = new ExecutionScope(List.of(event), customResource, null); + ExecutionScope executionScope = new ExecutionScope(customResource, null); PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); @@ -160,7 +123,6 @@ public void executesTheControllerInstantlyAfterErrorIfEventsBuffered() { .handleExecution(executionScopeArgumentCaptor.capture()); List allValues = executionScopeArgumentCaptor.getAllValues(); assertThat(allValues).hasSize(2); - assertThat(allValues.get(1).getEvents()).hasSize(2); verify(retryTimerEventSourceMock, never()) .scheduleOnce(eq(customResource), eq(GenericRetry.DEFAULT_INITIAL_INTERVAL)); } @@ -238,12 +200,29 @@ public void doNotFireEventsIfClosing() { verify(eventDispatcherMock, timeout(50).times(0)).handleExecution(any()); } - private void waitMinimalTime() { - try { - Thread.sleep(50); - } catch (InterruptedException e) { - throw new IllegalStateException(e); - } + @Test + public void cleansUpWhenDeleteEventReceivedAndNoEventPresent() { + Event deleteEvent = + new CustomResourceEvent(DELETED, prepareCREvent().getRelatedCustomResourceID()); + + defaultEventHandler.handleEvent(deleteEvent); + + verify(defaultEventSourceManagerMock, times(1)) + .cleanupForCustomResource(eq(deleteEvent.getRelatedCustomResourceID())); + } + + @Test + public void cleansUpAfterExecutionIfOnlyDeleteEventMarkLeft() { + var cr = testCustomResource(new CustomResourceID(UUID.randomUUID().toString())); + var crEvent = prepareCREvent(CustomResourceID.fromResource(cr)); + eventMarker.markDeleteEventReceived(crEvent.getRelatedCustomResourceID()); + var executionScope = new ExecutionScope(cr, null); + + defaultEventHandler.eventProcessingFinished(executionScope, + PostExecutionControl.defaultDispatch()); + + verify(defaultEventSourceManagerMock, times(1)) + .cleanupForCustomResource(eq(crEvent.getRelatedCustomResourceID())); } private CustomResourceID eventAlreadyUnderProcessing() { @@ -265,11 +244,12 @@ private CustomResourceEvent prepareCREvent() { private CustomResourceEvent prepareCREvent(CustomResourceID uid) { TestCustomResource customResource = testCustomResource(uid); when(resourceCache.getCustomResource(eq(uid))).thenReturn(Optional.of(customResource)); - return new CustomResourceEvent(ResourceAction.UPDATED, customResource); + return new CustomResourceEvent(ResourceAction.UPDATED, + CustomResourceID.fromResource(customResource)); } private Event nonCREvent(CustomResourceID relatedCustomResourceUid) { - return new TimerEvent(relatedCustomResourceUid); + return new DefaultEvent(relatedCustomResourceUid); } private void overrideData(CustomResourceID id, CustomResource applyTo) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java deleted file mode 100644 index 6dc16f8a69..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventBufferTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.javaoperatorsdk.operator.processing; - -import java.util.List; -import java.util.UUID; - -import org.junit.jupiter.api.Test; - -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; - -import static org.assertj.core.api.Assertions.assertThat; - -class EventBufferTest { - - private EventBuffer eventBuffer = new EventBuffer(); - - String name = UUID.randomUUID().toString(); - CustomResourceID customResourceID = new CustomResourceID(name); - Event testEvent1 = new TimerEvent(customResourceID); - Event testEvent2 = new TimerEvent(customResourceID); - - @Test - public void storesEvents() { - eventBuffer.addEvent(testEvent1); - eventBuffer.addEvent(testEvent2); - - assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceID())).isTrue(); - List events = eventBuffer.getAndRemoveEventsForExecution(customResourceID); - assertThat(events).hasSize(2); - } - - @Test - public void getsAndRemovesEvents() { - eventBuffer.addEvent(testEvent1); - eventBuffer.addEvent(testEvent2); - - List events = eventBuffer.getAndRemoveEventsForExecution(new CustomResourceID(name)); - assertThat(events).hasSize(2); - assertThat(events).contains(testEvent1, testEvent2); - } - - @Test - public void checksIfThereAreStoredEvents() { - eventBuffer.addEvent(testEvent1); - eventBuffer.addEvent(testEvent2); - - assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceID())).isTrue(); - } - - @Test - public void canClearEvents() { - eventBuffer.addEvent(testEvent1); - eventBuffer.addEvent(testEvent2); - - eventBuffer.cleanup(customResourceID); - - assertThat(eventBuffer.containsEvents(testEvent1.getRelatedCustomResourceID())).isFalse(); - } -} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index 00693e2f34..b709c026e1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -19,6 +19,7 @@ import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; @@ -267,7 +268,6 @@ void propagatesRetryInfoToContextIfFinalizerSet() { eventDispatcher.handleExecution( new ExecutionScope( - List.of(), testCustomResource, new RetryInfo() { @Override @@ -330,10 +330,11 @@ private void removeFinalizers(CustomResource customResource) { public ExecutionScope executionScopeWithCREvent( ResourceAction action, CustomResource resource, Event... otherEvents) { - CustomResourceEvent event = new CustomResourceEvent(action, resource); + CustomResourceEvent event = + new CustomResourceEvent(action, CustomResourceID.fromResource(resource)); List eventList = new ArrayList<>(1 + otherEvents.length); eventList.add(event); eventList.addAll(Arrays.asList(otherEvents)); - return new ExecutionScope(eventList, resource, null); + return new ExecutionScope(resource, null); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java new file mode 100644 index 0000000000..98b87f1713 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java @@ -0,0 +1,67 @@ +package io.javaoperatorsdk.operator.processing; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; + +import static org.assertj.core.api.Assertions.assertThat; + +class EventMarkerTest { + + private final EventMarker eventMarker = new EventMarker(); + private CustomResourceID sampleCustomResourceID = new CustomResourceID("test-name"); + + @Test + public void returnsNoEventPresentIfNotMarkedYet() { + assertThat(eventMarker.noEventPresent(sampleCustomResourceID)).isTrue(); + } + + @Test + public void marksEvent() { + eventMarker.markEventReceived(sampleCustomResourceID); + + assertThat(eventMarker.eventPresent(sampleCustomResourceID)).isTrue(); + assertThat(eventMarker.deleteEventPresent(sampleCustomResourceID)).isFalse(); + } + + @Test + public void marksDeleteEvent() { + eventMarker.markDeleteEventReceived(sampleCustomResourceID); + + assertThat(eventMarker.deleteEventPresent(sampleCustomResourceID)) + .isTrue(); + assertThat(eventMarker.eventPresent(sampleCustomResourceID)).isFalse(); + } + + @Test + public void afterDeleteEventMarkEventIsNotRelevant() { + eventMarker.markEventReceived(sampleCustomResourceID); + + eventMarker.markDeleteEventReceived(sampleCustomResourceID); + + assertThat(eventMarker.deleteEventPresent(sampleCustomResourceID)) + .isTrue(); + assertThat(eventMarker.eventPresent(sampleCustomResourceID)).isFalse(); + } + + @Test + public void cleansUp() { + eventMarker.markEventReceived(sampleCustomResourceID); + eventMarker.markDeleteEventReceived(sampleCustomResourceID); + + eventMarker.cleanup(sampleCustomResourceID); + + assertThat(eventMarker.deleteEventPresent(sampleCustomResourceID)).isFalse(); + assertThat(eventMarker.eventPresent(sampleCustomResourceID)).isFalse(); + } + + @Test + public void cannotMarkEventAfterDeleteEventReceived() { + Assertions.assertThrows(IllegalStateException.class, () -> { + eventMarker.markDeleteEventReceived(sampleCustomResourceID); + eventMarker.markEventReceived(sampleCustomResourceID); + }); + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java deleted file mode 100644 index dc65981cff..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventListTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event; - -import java.util.Arrays; - -import org.junit.jupiter.api.Test; - -import io.javaoperatorsdk.operator.processing.event.internal.TimerEvent; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -class EventListTest { - - @Test - public void returnsLatestOfEventType() { - TimerEvent event2 = new TimerEvent(new CustomResourceID("name1")); - EventList eventList = - new EventList( - Arrays.asList(mock(Event.class), new TimerEvent(new CustomResourceID("name2")), event2, - mock(Event.class))); - - assertThat(eventList.getLatestOfType(TimerEvent.class).get()).isEqualTo(event2); - } -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java index 27b651c9fb..33e590f75f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java @@ -41,7 +41,6 @@ public UpdateControl createOrUpdateResource( timerEventSource.schedule(resource, TIMER_DELAY, TIMER_PERIOD); - log.info("Events:: " + context.getEvents()); numberOfExecutions.addAndGet(1); ensureStatusExists(resource); resource.getStatus().setState(EventSourceTestCustomResourceStatus.State.SUCCESS); From b278ebf6dd60a7e8d2392738dd10d1af1118df45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 20 Oct 2021 15:49:04 +0200 Subject: [PATCH 0102/1608] fix: cache handling on update (#604) Co-authored-by: Chris Laprun --- .../processing/DefaultEventHandler.java | 49 ++++++++++++-- .../processing/event/CustomResourceID.java | 8 +++ .../event/DefaultEventSourceManager.java | 2 +- .../internal/CustomResourceEventSource.java | 41 +++++++++--- .../OnceWhitelistEventFilterEventFilter.java | 36 ++++++++++ .../javaoperatorsdk/operator/TestUtils.java | 1 + .../processing/DefaultEventHandlerTest.java | 67 +++++++++++++++++-- .../CustomResourceEventSourceTest.java | 29 ++++++-- ...ceWhitelistEventFilterEventFilterTest.java | 43 ++++++++++++ 9 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 608a47567b..d466eef95d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -27,6 +27,7 @@ import io.javaoperatorsdk.operator.processing.retry.RetryExecution; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; +import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; /** * Event handler that makes sure that events are processed in a "single threaded" way per resource @@ -188,18 +189,18 @@ void eventProcessingFinished( if (!running) { return; } - + CustomResourceID customResourceID = executionScope.getCustomResourceID(); log.debug( "Event processing finished. Scope: {}, PostExecutionControl: {}", executionScope, postExecutionControl); - unsetUnderExecution(executionScope.getCustomResourceID()); + unsetUnderExecution(customResourceID); // If a delete event present at this phase, it was received during reconciliation. // So we either removed the finalizer during reconciliation or we don't use finalizers. // Either way we don't want to retry. if (retry != null && postExecutionControl.exceptionDuringExecution() && - !eventMarker.deleteEventPresent(executionScope.getCustomResourceID())) { + !eventMarker.deleteEventPresent(customResourceID)) { handleRetryOnException(executionScope); // todo revisit monitoring since events are not present anymore // final var monitor = monitor(); executionScope.getEvents().forEach(e -> @@ -210,11 +211,15 @@ void eventProcessingFinished( if (retry != null) { handleSuccessfulExecutionRegardingRetry(executionScope); } - if (eventMarker.deleteEventPresent(executionScope.getCustomResourceID())) { + if (eventMarker.deleteEventPresent(customResourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { - if (eventMarker.eventPresent(executionScope.getCustomResourceID())) { - submitReconciliationExecution(executionScope.getCustomResourceID()); + if (eventMarker.eventPresent(customResourceID)) { + if (isCacheReadyForInstantReconciliation(executionScope, postExecutionControl)) { + submitReconciliationExecution(customResourceID); + } else { + postponeReconciliationAndHandleCacheSyncEvent(customResourceID); + } } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getCustomResource()); @@ -225,6 +230,38 @@ void eventProcessingFinished( } } + private void postponeReconciliationAndHandleCacheSyncEvent(CustomResourceID customResourceID) { + eventSourceManager.getCustomResourceEventSource().whitelistNextEvent(customResourceID); + } + + private boolean isCacheReadyForInstantReconciliation(ExecutionScope executionScope, + PostExecutionControl postExecutionControl) { + if (!postExecutionControl.customResourceUpdatedDuringExecution()) { + return true; + } + String originalResourceVersion = getVersion(executionScope.getCustomResource()); + String customResourceVersionAfterExecution = getVersion(postExecutionControl + .getUpdatedCustomResource() + .orElseThrow(() -> new IllegalStateException( + "Updated custom resource must be present at this point of time"))); + String cachedCustomResourceVersion = getVersion(resourceCache + .getCustomResource(executionScope.getCustomResourceID()) + .orElseThrow(() -> new IllegalStateException( + "Cached custom resource must be present at this point"))); + + if (cachedCustomResourceVersion.equals(customResourceVersionAfterExecution)) { + return true; + } + if (cachedCustomResourceVersion.equals(originalResourceVersion)) { + return false; + } + // If the cached resource version equals neither the version before nor after execution + // probably an update happened on the custom resource independent of the framework during + // reconciliation. We cannot tell at this point if it happened before our update or before. + // (Well we could if we would parse resource version, but that should not be done by definition) + return true; + } + private void reScheduleExecutionIfInstructed(PostExecutionControl postExecutionControl, R customResource) { postExecutionControl.getReScheduleDelay().ifPresent(delay -> eventSourceManager diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java index d405e48a5a..b668a772dd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java @@ -47,4 +47,12 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(name, namespace); } + + @Override + public String toString() { + return "CustomResourceID{" + + "name='" + name + '\'' + + ", namespace='" + namespace + '\'' + + '}'; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index ef76244de3..02f27a010a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -108,7 +108,7 @@ public Set getRegisteredEventSources() { } @Override - public CustomResourceEventSource getCustomResourceEventSource() { + public CustomResourceEventSource getCustomResourceEventSource() { return customResourceEventSource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 3b56e5b685..58e1b3bf54 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -41,11 +41,31 @@ public class CustomResourceEventSource> extends A private final Map> sharedIndexInformers = new ConcurrentHashMap<>(); private final ObjectMapper cloningObjectMapper; + private final CustomResourceEventFilter filter; + private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; + public CustomResourceEventSource(ConfiguredController controller) { this.controller = controller; this.cloningObjectMapper = controller.getConfiguration().getConfigurationService().getObjectMapper(); + + var filters = new CustomResourceEventFilter[] { + CustomResourceEventFilters.finalizerNeededAndApplied(), + CustomResourceEventFilters.markedForDeletion(), + CustomResourceEventFilters.and( + controller.getConfiguration().getEventFilter(), + CustomResourceEventFilters.generationAware()), + null + }; + + if (controller.getConfiguration().isGenerationAware()) { + onceWhitelistEventFilterEventFilter = new OnceWhitelistEventFilterEventFilter<>(); + filters[filters.length - 1] = onceWhitelistEventFilterEventFilter; + } else { + onceWhitelistEventFilterEventFilter = null; + } + filter = CustomResourceEventFilters.or(filters); } @Override @@ -90,7 +110,7 @@ public void start() { @Override public void close() throws IOException { eventHandler.close(); - for (SharedIndexInformer informer : sharedIndexInformers.values()) { + for (SharedIndexInformer informer : sharedIndexInformers.values()) { try { log.info("Closing informer {} -> {}", controller, informer); informer.close(); @@ -104,13 +124,6 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource log.debug( "Event received for resource: {}", getName(customResource)); - final CustomResourceEventFilter filter = CustomResourceEventFilters.or( - CustomResourceEventFilters.finalizerNeededAndApplied(), - CustomResourceEventFilters.markedForDeletion(), - CustomResourceEventFilters.and( - controller.getConfiguration().getEventFilter(), - CustomResourceEventFilters.generationAware())); - if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { eventHandler.handleEvent( new CustomResourceEvent(action, CustomResourceID.fromResource(customResource))); @@ -171,4 +184,16 @@ private T clone(T customResource) { throw new IllegalStateException(e); } } + + /** + * This will ensure that the next event received after this method is called will not be filtered + * out. + * + * @param customResourceID - to which the event is related + */ + public void whitelistNextEvent(CustomResourceID customResourceID) { + if (onceWhitelistEventFilterEventFilter != null) { + onceWhitelistEventFilterEventFilter.whitelistNextEvent(customResourceID); + } + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java new file mode 100644 index 0000000000..4c144e65e6 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; + +public class OnceWhitelistEventFilterEventFilter> + implements CustomResourceEventFilter { + + private static final Logger log = + LoggerFactory.getLogger(OnceWhitelistEventFilterEventFilter.class); + + private final ConcurrentMap whiteList = + new ConcurrentHashMap<>(); + + @Override + public boolean acceptChange(ControllerConfiguration configuration, T oldResource, + T newResource) { + CustomResourceID customResourceID = CustomResourceID.fromResource(newResource); + boolean res = whiteList.remove(customResourceID, customResourceID); + if (res) { + log.debug("Accepting whitelisted event for CR id: {}", customResourceID); + } + return res; + } + + public void whitelistNextEvent(CustomResourceID customResourceID) { + whiteList.putIfAbsent(customResourceID, customResourceID); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index c1e887de86..49dc35abdb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -32,6 +32,7 @@ public static TestCustomResource testCustomResource(CustomResourceID id) { resource.setMetadata( new ObjectMetaBuilder() .withName(id.getName()) + .withResourceVersion("1") .withGeneration(1L) .withNamespace(id.getNamespace().orElse(null)) .build()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 1ecc551951..bc1b43fb21 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -17,6 +17,7 @@ import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -39,15 +40,15 @@ class DefaultEventHandlerTest { private EventDispatcher eventDispatcherMock = mock(EventDispatcher.class); private DefaultEventSourceManager defaultEventSourceManagerMock = mock(DefaultEventSourceManager.class); - private ResourceCache resourceCache = mock(ResourceCache.class); + private ResourceCache resourceCacheMock = mock(ResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private DefaultEventHandler defaultEventHandler = - new DefaultEventHandler(eventDispatcherMock, resourceCache, "Test", null, eventMarker); + new DefaultEventHandler(eventDispatcherMock, resourceCacheMock, "Test", null, eventMarker); private DefaultEventHandler defaultEventHandlerWithRetry = - new DefaultEventHandler(eventDispatcherMock, resourceCache, "Test", + new DefaultEventHandler(eventDispatcherMock, resourceCacheMock, "Test", GenericRetry.defaultLimitedExponentialRetry(), eventMarker); @BeforeEach @@ -68,7 +69,7 @@ public void dispatchesEventsIfNoExecutionInProgress() { @Test public void skipProcessingIfLatestCustomResourceNotInCache() { Event event = prepareCREvent(); - when(resourceCache.getCustomResource(event.getRelatedCustomResourceID())) + when(resourceCacheMock.getCustomResource(event.getRelatedCustomResourceID())) .thenReturn(Optional.empty()); defaultEventHandler.handleEvent(event); @@ -213,7 +214,7 @@ public void cleansUpWhenDeleteEventReceivedAndNoEventPresent() { @Test public void cleansUpAfterExecutionIfOnlyDeleteEventMarkLeft() { - var cr = testCustomResource(new CustomResourceID(UUID.randomUUID().toString())); + var cr = testCustomResource(); var crEvent = prepareCREvent(CustomResourceID.fromResource(cr)); eventMarker.markDeleteEventReceived(crEvent.getRelatedCustomResourceID()); var executionScope = new ExecutionScope(cr, null); @@ -225,6 +226,60 @@ public void cleansUpAfterExecutionIfOnlyDeleteEventMarkLeft() { .cleanupForCustomResource(eq(crEvent.getRelatedCustomResourceID())); } + @Test + public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { + var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); + var cr = testCustomResource(crID); + var updatedCr = testCustomResource(crID); + updatedCr.getMetadata().setResourceVersion("2"); + var mockCREventSource = mock(CustomResourceEventSource.class); + eventMarker.markEventReceived(crID); + when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); + when(defaultEventSourceManagerMock.getCustomResourceEventSource()) + .thenReturn(mockCREventSource); + + defaultEventHandler.eventProcessingFinished(new ExecutionScope(cr, null), + PostExecutionControl.customResourceUpdated(updatedCr)); + + verify(mockCREventSource, times(1)).whitelistNextEvent(eq(crID)); + } + + @Test + public void dontWhitelistsEventWhenOtherChangeDuringExecution() { + var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); + var cr = testCustomResource(crID); + var updatedCr = testCustomResource(crID); + updatedCr.getMetadata().setResourceVersion("2"); + var otherChangeCR = testCustomResource(crID); + otherChangeCR.getMetadata().setResourceVersion("3"); + var mockCREventSource = mock(CustomResourceEventSource.class); + eventMarker.markEventReceived(crID); + when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(otherChangeCR)); + when(defaultEventSourceManagerMock.getCustomResourceEventSource()) + .thenReturn(mockCREventSource); + + defaultEventHandler.eventProcessingFinished(new ExecutionScope(cr, null), + PostExecutionControl.customResourceUpdated(updatedCr)); + + verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); + } + + @Test + public void dontWhitelistsEventIfUpdatedEventInCache() { + var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); + var cr = testCustomResource(crID); + var mockCREventSource = mock(CustomResourceEventSource.class); + eventMarker.markEventReceived(crID); + when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); + when(defaultEventSourceManagerMock.getCustomResourceEventSource()) + .thenReturn(mockCREventSource); + + defaultEventHandler.eventProcessingFinished(new ExecutionScope(cr, null), + PostExecutionControl.customResourceUpdated(cr)); + + verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); + } + private CustomResourceID eventAlreadyUnderProcessing() { when(eventDispatcherMock.handleExecution(any())) .then( @@ -243,7 +298,7 @@ private CustomResourceEvent prepareCREvent() { private CustomResourceEvent prepareCREvent(CustomResourceID uid) { TestCustomResource customResource = testCustomResource(uid); - when(resourceCache.getCustomResource(eq(uid))).thenReturn(Optional.of(customResource)); + when(resourceCacheMock.getCustomResource(eq(uid))).thenReturn(Optional.of(customResource)); return new CustomResourceEvent(ResourceAction.UPDATED, CustomResourceID.fromResource(customResource)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 4bf7d1afd7..0bd736b6c2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -14,6 +14,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -103,16 +104,36 @@ public void handlesAllEventIfNotGenerationAware() { } @Test - public void eventNotMarkedForLastGenerationIfNoFinalizer() { + public void eventWithNoGenerationProcessedIfNoFinalizer() { TestCustomResource customResource1 = TestUtils.testCustomResource(); customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); + verify(eventHandler, times(1)).handleEvent(any()); + } - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); - verify(eventHandler, times(2)).handleEvent(any()); + @Test + public void handlesNextEventIfWhitelisted() { + TestCustomResource customResource = TestUtils.testCustomResource(); + customResource.getMetadata().setFinalizers(List.of(FINALIZER)); + customResourceEventSource.whitelistNextEvent(CustomResourceID.fromResource(customResource)); + + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, + customResource); + + verify(eventHandler, times(1)).handleEvent(any()); + } + + @Test + public void notHandlesNextEventIfNotWhitelisted() { + TestCustomResource customResource = TestUtils.testCustomResource(); + customResource.getMetadata().setFinalizers(List.of(FINALIZER)); + + customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, + customResource); + + verify(eventHandler, times(0)).handleEvent(any()); } private static class TestConfiguredController extends ConfiguredController { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java new file mode 100644 index 0000000000..fde6bb2a7f --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java @@ -0,0 +1,43 @@ +package io.javaoperatorsdk.operator.processing.event.internal; + +import org.junit.jupiter.api.Test; + +import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; + +import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; +import static org.assertj.core.api.Assertions.assertThat; + +class OnceWhitelistEventFilterEventFilterTest { + + private OnceWhitelistEventFilterEventFilter filter = new OnceWhitelistEventFilterEventFilter<>(); + + @Test + public void notAcceptCustomResourceNotWhitelisted() { + assertThat(filter.acceptChange(null, + testCustomResource(), testCustomResource())).isFalse(); + } + + @Test + public void allowCustomResourceWhitelisted() { + var cr = TestUtils.testCustomResource(); + + filter.whitelistNextEvent(CustomResourceID.fromResource(cr)); + + assertThat(filter.acceptChange(null, + cr, cr)).isTrue(); + } + + @Test + public void allowCustomResourceWhitelistedOnlyOnce() { + var cr = TestUtils.testCustomResource(); + + filter.whitelistNextEvent(CustomResourceID.fromResource(cr)); + + assertThat(filter.acceptChange(null, + cr, cr)).isTrue(); + assertThat(filter.acceptChange(null, + cr, cr)).isFalse(); + } + +} From e9d8c351bae01523f2f85778a333e884335cad7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 20 Oct 2021 16:15:26 +0200 Subject: [PATCH 0103/1608] feat: Only one scheduled event (re-schedul / retry) at one time (#609) --- .../processing/DefaultEventHandler.java | 25 +++++++++++-------- .../event/DefaultEventSourceManager.java | 10 ++++---- .../internal/CustomResourceEventSource.java | 2 +- .../processing/DefaultEventHandlerTest.java | 13 +++++++++- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index d466eef95d..5c21747623 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -199,7 +199,7 @@ void eventProcessingFinished( // If a delete event present at this phase, it was received during reconciliation. // So we either removed the finalizer during reconciliation or we don't use finalizers. // Either way we don't want to retry. - if (retry != null && postExecutionControl.exceptionDuringExecution() && + if (isRetryConfigured() && postExecutionControl.exceptionDuringExecution() && !eventMarker.deleteEventPresent(customResourceID)) { handleRetryOnException(executionScope); // todo revisit monitoring since events are not present anymore @@ -207,10 +207,7 @@ void eventProcessingFinished( // monitor.failedEvent(executionScope.getCustomResourceID(), e)); return; } - - if (retry != null) { - handleSuccessfulExecutionRegardingRetry(executionScope); - } + cleanupOnSuccessfulExecution(executionScope); if (eventMarker.deleteEventPresent(customResourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { @@ -265,7 +262,7 @@ private boolean isCacheReadyForInstantReconciliation(ExecutionScope execution private void reScheduleExecutionIfInstructed(PostExecutionControl postExecutionControl, R customResource) { postExecutionControl.getReScheduleDelay().ifPresent(delay -> eventSourceManager - .getRetryTimerEventSource() + .getRetryAndRescheduleTimerEventSource() .scheduleOnce(customResource, delay)); } @@ -295,19 +292,21 @@ private void handleRetryOnException(ExecutionScope executionScope) { delay, customResourceID); eventSourceManager - .getRetryTimerEventSource() + .getRetryAndRescheduleTimerEventSource() .scheduleOnce(executionScope.getCustomResource(), delay); }, () -> log.error("Exhausted retries for {}", executionScope)); } - private void handleSuccessfulExecutionRegardingRetry(ExecutionScope executionScope) { + private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) { log.debug( - "Marking successful execution for resource: {}", + "Cleanup for successful execution for resource: {}", getName(executionScope.getCustomResource())); - retryState.remove(executionScope.getCustomResourceID()); + if (isRetryConfigured()) { + retryState.remove(executionScope.getCustomResourceID()); + } eventSourceManager - .getRetryTimerEventSource() + .getRetryAndRescheduleTimerEventSource() .cancelOnceSchedule(executionScope.getCustomResourceID()); } @@ -337,6 +336,10 @@ private void unsetUnderExecution(CustomResourceID customResourceUid) { underProcessing.remove(customResourceUid); } + private boolean isRetryConfigured() { + return retry != null; + } + @Override public void close() { lock.lock(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 02f27a010a..d72bd6ec7e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -22,7 +22,7 @@ public class DefaultEventSourceManager> private final ReentrantLock lock = new ReentrantLock(); private final Set eventSources = Collections.synchronizedSet(new HashSet<>()); private DefaultEventHandler defaultEventHandler; - private TimerEventSource retryTimerEventSource; + private TimerEventSource retryAndRescheduleTimerEventSource; private CustomResourceEventSource customResourceEventSource; DefaultEventSourceManager(DefaultEventHandler defaultEventHandler) { @@ -39,8 +39,8 @@ private void init(DefaultEventHandler defaultEventHandler) { this.defaultEventHandler = defaultEventHandler; defaultEventHandler.setEventSourceManager(this); - this.retryTimerEventSource = new TimerEventSource<>(); - registerEventSource(retryTimerEventSource); + this.retryAndRescheduleTimerEventSource = new TimerEventSource<>(); + registerEventSource(retryAndRescheduleTimerEventSource); } @Override @@ -98,8 +98,8 @@ public void cleanupForCustomResource(CustomResourceID customResourceUid) { } } - public TimerEventSource getRetryTimerEventSource() { - return retryTimerEventSource; + public TimerEventSource getRetryAndRescheduleTimerEventSource() { + return retryAndRescheduleTimerEventSource; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 58e1b3bf54..ddbbfde692 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -188,7 +188,7 @@ private T clone(T customResource) { /** * This will ensure that the next event received after this method is called will not be filtered * out. - * + * * @param customResourceID - to which the event is related */ public void whitelistNextEvent(CustomResourceID customResourceID) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index bc1b43fb21..f231852537 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -53,7 +53,7 @@ class DefaultEventHandlerTest { @BeforeEach public void setup() { - when(defaultEventSourceManagerMock.getRetryTimerEventSource()) + when(defaultEventSourceManagerMock.getRetryAndRescheduleTimerEventSource()) .thenReturn(retryTimerEventSourceMock); defaultEventHandler.setEventSourceManager(defaultEventSourceManagerMock); defaultEventHandlerWithRetry.setEventSourceManager(defaultEventSourceManagerMock); @@ -280,6 +280,17 @@ public void dontWhitelistsEventIfUpdatedEventInCache() { verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); } + @Test + public void cancelScheduleOnceEventsOnSuccessfulExecution() { + var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); + var cr = testCustomResource(crID); + + defaultEventHandler.eventProcessingFinished(new ExecutionScope(cr, null), + PostExecutionControl.defaultDispatch()); + + verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); + } + private CustomResourceID eventAlreadyUnderProcessing() { when(eventDispatcherMock.handleExecution(any())) .then( From 892880b5f83ce0edbe6db2afe13c8e3f547afe01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 20 Oct 2021 16:40:56 +0200 Subject: [PATCH 0104/1608] feat: Cloner interface for Custom Resource instead of ObjectMapper (#611) --- .../operator/api/config/Cloner.java | 9 +++++++ .../api/config/ConfigurationService.java | 24 +++++++++++++------ .../config/ConfigurationServiceOverrider.java | 14 +++++------ .../internal/CustomResourceEventSource.java | 23 +++++------------- .../CustomResourceEventFilterTest.java | 8 +++---- .../CustomResourceEventSourceTest.java | 4 ++-- 6 files changed, 44 insertions(+), 38 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java new file mode 100644 index 0000000000..92f569f36a --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.client.CustomResource; + +public interface Cloner { + + CustomResource clone(CustomResource object); + +} 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 cb16793f43..ec61108bf2 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 @@ -9,12 +9,24 @@ import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.ResourceController; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { - ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + Cloner DEFAULT_CLONER = new Cloner() { + private final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Override + public CustomResource clone(CustomResource object) { + try { + return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(object), object.getClass()); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + } + }; /** * Retrieves the configuration associated with the specified controller @@ -79,14 +91,12 @@ default int concurrentReconciliationThreads() { } /** - * The {@link ObjectMapper} that the operator should use to de-/serialize resources. This is - * particularly useful when frameworks can configure a specific mapper that should also be used by - * the SDK. - * + * Used to clone custom resources. + * * @return the ObjectMapper to use */ - default ObjectMapper getObjectMapper() { - return OBJECT_MAPPER; + default Cloner getResourceCloner() { + return DEFAULT_CLONER; } int DEFAULT_TERMINATION_TIMEOUT_SECONDS = 10; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index ed0bd513c6..f1faef44f8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -7,15 +7,13 @@ import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.ResourceController; -import com.fasterxml.jackson.databind.ObjectMapper; - public class ConfigurationServiceOverrider { private final ConfigurationService original; private Metrics metrics; private Config clientConfig; private boolean checkCR; private int threadNumber; - private ObjectMapper mapper; + private Cloner cloner; private int timeoutSeconds; public ConfigurationServiceOverrider( @@ -24,7 +22,7 @@ public ConfigurationServiceOverrider( this.clientConfig = original.getClientConfiguration(); this.checkCR = original.checkCRDAndValidateLocalModel(); this.threadNumber = original.concurrentReconciliationThreads(); - this.mapper = original.getObjectMapper(); + this.cloner = original.getResourceCloner(); this.timeoutSeconds = original.getTerminationTimeoutSeconds(); this.metrics = original.getMetrics(); } @@ -45,8 +43,8 @@ public ConfigurationServiceOverrider withConcurrentReconciliationThreads(int thr return this; } - public ConfigurationServiceOverrider withObjectMapper(ObjectMapper mapper) { - this.mapper = mapper; + public ConfigurationServiceOverrider withResourceCloner(Cloner cloner) { + this.cloner = cloner; return this; } @@ -94,8 +92,8 @@ public int concurrentReconciliationThreads() { } @Override - public ObjectMapper getObjectMapper() { - return mapper; + public Cloner getResourceCloner() { + return cloner; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index ddbbfde692..b9c4c5b6a3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -13,15 +13,13 @@ import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.ResourceCache; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; @@ -40,15 +38,14 @@ public class CustomResourceEventSource> extends A private final ConfiguredController controller; private final Map> sharedIndexInformers = new ConcurrentHashMap<>(); - private final ObjectMapper cloningObjectMapper; + private final CustomResourceEventFilter filter; private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; - + private final Cloner cloner; public CustomResourceEventSource(ConfiguredController controller) { this.controller = controller; - this.cloningObjectMapper = - controller.getConfiguration().getConfigurationService().getObjectMapper(); + this.cloner = controller.getConfiguration().getConfigurationService().getResourceCloner(); var filters = new CustomResourceEventFilter[] { CustomResourceEventFilters.finalizerNeededAndApplied(), @@ -160,7 +157,7 @@ public Optional getCustomResource(CustomResourceID resourceID) { if (resource == null) { return Optional.empty(); } else { - return Optional.of(clone(resource)); + return Optional.of((T) (cloner.clone(resource))); } } @@ -176,15 +173,6 @@ public SharedIndexInformer getInformer(String namespace) { return getInformers().get(Objects.requireNonNullElse(namespace, ANY_NAMESPACE_MAP_KEY)); } - private T clone(T customResource) { - try { - return (T) cloningObjectMapper.readValue( - cloningObjectMapper.writeValueAsString(customResource), customResource.getClass()); - } catch (JsonProcessingException e) { - throw new IllegalStateException(e); - } - } - /** * This will ensure that the next event received after this method is called will not be filtered * out. @@ -196,4 +184,5 @@ public void whitelistNextEvent(CustomResourceID customResourceID) { onceWhitelistEventFilterEventFilter.whitelistNextEvent(customResourceID); } } + } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java index bd0d2ed72d..3e9e413f1e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java @@ -103,8 +103,8 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { oldResource.getStatus().getConfigMapStatus(), newResource.getStatus().getConfigMapStatus())); - when(config.getConfigurationService().getObjectMapper()) - .thenReturn(ConfigurationService.OBJECT_MAPPER); + when(config.getConfigurationService().getResourceCloner()) + .thenReturn(ConfigurationService.DEFAULT_CLONER); var controller = new TestConfiguredController(config); var eventSource = new CustomResourceEventSource<>(controller); @@ -142,8 +142,8 @@ public TestControllerConfig(String finalizer, boolean generationAware, TestCustomResource.class, mock(ConfigurationService.class)); - when(getConfigurationService().getObjectMapper()) - .thenReturn(ConfigurationService.OBJECT_MAPPER); + when(getConfigurationService().getResourceCloner()) + .thenReturn(ConfigurationService.DEFAULT_CLONER); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index 0bd736b6c2..a24c32a547 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -164,8 +164,8 @@ public TestConfiguration(boolean generationAware) { null, TestCustomResource.class, mock(ConfigurationService.class)); - when(getConfigurationService().getObjectMapper()) - .thenReturn(ConfigurationService.OBJECT_MAPPER); + when(getConfigurationService().getResourceCloner()) + .thenReturn(ConfigurationService.DEFAULT_CLONER); when(getConfigurationService().getMetrics()) .thenReturn(Metrics.NOOP); } From 3a10383c64deb64ec332abc79f6864a717f13ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 26 Oct 2021 11:16:53 +0200 Subject: [PATCH 0105/1608] fix: EventSourceManager and ResourceController interface enhancements (#618) --- .../operator/api/EventSourceInitializer.java | 16 ++++++++++++++++ .../operator/api/ResourceController.java | 8 -------- .../processing/ConfiguredController.java | 15 +++++++-------- .../event/DefaultEventSourceManager.java | 3 ++- .../processing/event/EventSourceManager.java | 6 +----- .../EventSourceTestCustomResourceController.java | 10 ++++------ ...rEventSourceTestCustomResourceController.java | 10 ++++------ 7 files changed, 34 insertions(+), 34 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/EventSourceInitializer.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/EventSourceInitializer.java new file mode 100644 index 0000000000..e7500b9e47 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/EventSourceInitializer.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.api; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; + +public interface EventSourceInitializer> { + + /** + * In this typically you might want to register event sources. But can access + * CustomResourceEventSource, what might be handy for some edge cases. + * + * @param eventSourceManager the {@link EventSourceManager} where event sources can be registered. + */ + void prepareEventSources(EventSourceManager eventSourceManager); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java index 5779cc4eb9..817cb554fc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java @@ -1,7 +1,6 @@ package io.javaoperatorsdk.operator.api; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.EventSourceManager; public interface ResourceController { @@ -49,11 +48,4 @@ default DeleteControl deleteResource(R resource, Context context) { */ UpdateControl createOrUpdateResource(R resource, Context context); - /** - * In init typically you might want to register event sources. - * - * @param eventSourceManager the {@link EventSourceManager} which handles this controller and with - * which event sources can be registered - */ - default void init(EventSourceManager eventSourceManager) {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index 3ce90ebb67..dd2c74354e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -14,20 +14,17 @@ import io.javaoperatorsdk.operator.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; public class ConfiguredController> implements ResourceController, - Closeable { + Closeable, EventSourceInitializer { private final ResourceController controller; private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; - private EventSourceManager eventSourceManager; + private DefaultEventSourceManager eventSourceManager; public ConfiguredController(ResourceController controller, ControllerConfiguration configuration, @@ -97,7 +94,7 @@ public UpdateControl execute() { } @Override - public void init(EventSourceManager eventSourceManager) { + public void prepareEventSources(EventSourceManager eventSourceManager) { throw new UnsupportedOperationException("This method should never be called directly"); } @@ -169,7 +166,9 @@ public void start() throws OperatorException { try { eventSourceManager = new DefaultEventSourceManager<>(this); - controller.init(eventSourceManager); + if (controller instanceof EventSourceInitializer) { + ((EventSourceInitializer) controller).prepareEventSources(eventSourceManager); + } } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index d72bd6ec7e..c3ab127a50 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import java.io.Closeable; import java.util.*; import java.util.concurrent.locks.ReentrantLock; @@ -15,7 +16,7 @@ import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; public class DefaultEventSourceManager> - implements EventSourceManager { + implements EventSourceManager, Closeable { private static final Logger log = LoggerFactory.getLogger(DefaultEventSourceManager.class); 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 7e049e7c51..e06ab2e3d1 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,14 +1,12 @@ package io.javaoperatorsdk.operator.processing.event; -import java.io.Closeable; -import java.io.IOException; import java.util.Set; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; -public interface EventSourceManager> extends Closeable { +public interface EventSourceManager> { /** * Add the {@link EventSource} identified by the given name to the event manager. @@ -25,6 +23,4 @@ void registerEventSource(EventSource eventSource) CustomResourceEventSource getCustomResourceEventSource(); - @Override - default void close() throws IOException {} } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java index 33e590f75f..caa5ad9fda 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java @@ -7,17 +7,15 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @Controller public class EventSourceTestCustomResourceController - implements ResourceController, TestExecutionInfoProvider { + implements ResourceController, EventSourceInitializer, + TestExecutionInfoProvider { public static final String FINALIZER_NAME = ControllerUtils.getDefaultFinalizerName( @@ -31,7 +29,7 @@ public class EventSourceTestCustomResourceController new TimerEventSource<>(); @Override - public void init(EventSourceManager eventSourceManager) { + public void prepareEventSources(EventSourceManager eventSourceManager) { eventSourceManager.registerEventSource(timerEventSource); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java index a2284adfa1..772c93cac3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java @@ -5,10 +5,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; @@ -22,7 +19,8 @@ */ @Controller(finalizerName = NO_FINALIZER) public class InformerEventSourceTestCustomResourceController implements - ResourceController, KubernetesClientAware { + ResourceController, KubernetesClientAware, + EventSourceInitializer { private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomResourceController.class); @@ -34,7 +32,7 @@ public class InformerEventSourceTestCustomResourceController implements private InformerEventSource eventSource; @Override - public void init(EventSourceManager eventSourceManager) { + public void prepareEventSources(EventSourceManager eventSourceManager) { eventSource = new InformerEventSource<>(kubernetesClient, ConfigMap.class, Mappers.fromAnnotation(RELATED_RESOURCE_UID)); eventSourceManager.registerEventSource(eventSource); From 995e8dd97661cb5f382bc710ff24cd1cfe7c7a24 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 21 Oct 2021 11:11:32 +0200 Subject: [PATCH 0106/1608] feat!: adapt monitoring code to new implementation - record: * received events * deleted events * reconciliation attempts with retry information * reconciliation successes * reconciliation failure with exception information - move metrics-related code to monitoring package - cleaned-up obsolete/deprecated code --- .../micrometer/MicrometerMetrics.java | 84 ---------------- .../micrometer/MicrometerMetrics.java | 98 +++++++++++++++++++ .../operator/EventListUtils.java | 22 ----- .../io/javaoperatorsdk/operator/Metrics.java | 37 ------- .../api/config/ConfigurationService.java | 2 +- .../config/ConfigurationServiceOverrider.java | 2 +- .../operator/api/monitoring/Metrics.java | 42 ++++++++ .../processing/ConfiguredController.java | 2 +- .../processing/DefaultEventHandler.java | 90 +++++++---------- .../event/DefaultEventSourceManager.java | 6 +- .../processing/EventDispatcherTest.java | 2 +- .../CustomResourceEventSourceTest.java | 2 +- 12 files changed, 184 insertions(+), 205 deletions(-) delete mode 100644 micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java create mode 100644 micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java diff --git a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java deleted file mode 100644 index 3c8074f8f8..0000000000 --- a/micrometer-support/src/main/java/io/javaoperatorsdk/operator/micrometer/MicrometerMetrics.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.javaoperatorsdk.operator.micrometer; - -import java.util.Collections; -import java.util.Map; - -import io.javaoperatorsdk.operator.Metrics; -import io.javaoperatorsdk.operator.processing.DefaultEventHandler.EventMonitor; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; - -public class MicrometerMetrics implements Metrics { - - public static final String PREFIX = "operator.sdk."; - private final MeterRegistry registry; - private final EventMonitor monitor = new EventMonitor() { - @Override - public void processedEvent(CustomResourceID uid, Event event) { - incrementProcessedEventsNumber(); - } - - @Override - public void failedEvent(CustomResourceID uid, Event event) { - incrementControllerRetriesNumber(); - } - }; - - public MicrometerMetrics(MeterRegistry registry) { - this.registry = registry; - } - - public T timeControllerExecution(ControllerExecution execution) { - final var name = execution.controllerName(); - final var execName = PREFIX + "controllers.execution." + execution.name(); - final var timer = - Timer.builder(execName) - .tags("controller", name) - .publishPercentiles(0.3, 0.5, 0.95) - .publishPercentileHistogram() - .register(registry); - try { - final var result = timer.record(execution::execute); - final var successType = execution.successTypeName(result); - registry - .counter(execName + ".success", "controller", name, "type", successType) - .increment(); - return result; - } catch (Exception e) { - final var exception = e.getClass().getSimpleName(); - registry - .counter(execName + ".failure", "controller", name, "exception", exception) - .increment(); - throw e; - } - } - - public void incrementControllerRetriesNumber() { - registry - .counter( - PREFIX + "retry.on.exception", "retry", "retryCounter", "type", - "retryException") - .increment(); - - } - - public void incrementProcessedEventsNumber() { - registry - .counter( - PREFIX + "total.events.received", "events", "totalEvents", "type", - "eventsReceived") - .increment(); - - } - - public > T monitorSizeOf(T map, String name) { - return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); - } - - @Override - public EventMonitor getEventMonitor() { - return monitor; - } -} 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 new file mode 100644 index 0000000000..a90460d825 --- /dev/null +++ b/micrometer-support/src/main/java/io/javaoperatorsdk/operator/monitoring/micrometer/MicrometerMetrics.java @@ -0,0 +1,98 @@ +package io.javaoperatorsdk.operator.monitoring.micrometer; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import io.javaoperatorsdk.operator.api.RetryInfo; +import io.javaoperatorsdk.operator.api.monitoring.Metrics; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +public class MicrometerMetrics implements Metrics { + + private static final String PREFIX = "operator.sdk."; + private static final String RECONCILIATIONS = "reconciliations."; + private final MeterRegistry registry; + + public MicrometerMetrics(MeterRegistry registry) { + this.registry = registry; + } + + public T timeControllerExecution(ControllerExecution execution) { + final var name = execution.controllerName(); + final var execName = PREFIX + "controllers.execution." + execution.name(); + final var timer = + Timer.builder(execName) + .tags("controller", name) + .publishPercentiles(0.3, 0.5, 0.95) + .publishPercentileHistogram() + .register(registry); + try { + final var result = timer.record(execution::execute); + final var successType = execution.successTypeName(result); + registry + .counter(execName + ".success", "controller", name, "type", successType) + .increment(); + return result; + } catch (Exception e) { + final var exception = e.getClass().getSimpleName(); + registry + .counter(execName + ".failure", "controller", name, "exception", exception) + .increment(); + throw e; + } + } + + public void receivedEvent(Event event) { + incrementCounter(event.getRelatedCustomResourceID(), "events.received", "event", + event.getClass().getSimpleName()); + } + + @Override + public void cleanupDoneFor(CustomResourceID customResourceUid) { + incrementCounter(customResourceUid, "events.delete"); + } + + public void reconcileCustomResource(CustomResourceID customResourceID, + RetryInfo retryInfo) { + incrementCounter(customResourceID, RECONCILIATIONS + "started", + RECONCILIATIONS + "retries.number", "" + retryInfo.getAttemptCount(), + RECONCILIATIONS + "retries.last", "" + retryInfo.isLastAttempt()); + } + + @Override + public void finishedReconciliation(CustomResourceID customResourceID) { + incrementCounter(customResourceID, RECONCILIATIONS + "success"); + } + + public void failedReconciliation(CustomResourceID customResourceID, RuntimeException exception) { + var cause = exception.getCause(); + if (cause == null) { + cause = exception; + } else if (cause instanceof RuntimeException) { + cause = cause.getCause() != null ? cause.getCause() : cause; + } + incrementCounter(customResourceID, RECONCILIATIONS + "failed", "exception", + cause.getClass().getSimpleName()); + } + + public > T monitorSizeOf(T map, String name) { + return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); + } + + private void incrementCounter(CustomResourceID id, String counterName, String... additionalTags) { + var tags = List.of( + "name", id.getName(), + "name", id.getName(), "namespace", id.getNamespace().orElse(""), + "scope", id.getNamespace().isPresent() ? "namespace" : "cluster"); + if (additionalTags != null && additionalTags.length > 0) { + tags = new LinkedList<>(tags); + tags.addAll(List.of(additionalTags)); + } + registry.counter(PREFIX + counterName, tags.toArray(new String[0])).increment(); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java deleted file mode 100644 index 0fe18bdae0..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/EventListUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.javaoperatorsdk.operator; - -import java.util.List; - -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; - -public class EventListUtils { - - public static boolean containsCustomResourceDeletedEvent(List events) { - return events.stream() - .anyMatch( - e -> { - if (e instanceof CustomResourceEvent) { - return ((CustomResourceEvent) e).getAction() == ResourceAction.DELETED; - } else { - return false; - } - }); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java deleted file mode 100644 index a3f3ddccb5..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Metrics.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.javaoperatorsdk.operator; - -import java.util.Map; - -import io.javaoperatorsdk.operator.processing.DefaultEventHandler; -import io.javaoperatorsdk.operator.processing.DefaultEventHandler.EventMonitor; - -public interface Metrics { - Metrics NOOP = new Metrics() {}; - - - interface ControllerExecution { - String name(); - - String controllerName(); - - String successTypeName(T result); - - T execute(); - } - - default T timeControllerExecution(ControllerExecution execution) { - return execution.execute(); - } - - default void incrementControllerRetriesNumber() {} - - default void incrementProcessedEventsNumber() {} - - default > T monitorSizeOf(T map, String name) { - return map; - } - - default DefaultEventHandler.EventMonitor getEventMonitor() { - return EventMonitor.NOOP; - } -} 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 ec61108bf2..fb53f7eaae 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 @@ -6,8 +6,8 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.monitoring.Metrics; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index f1faef44f8..2cecfb552d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -4,8 +4,8 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.monitoring.Metrics; public class ConfigurationServiceOverrider { private final ConfigurationService original; 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 new file mode 100644 index 0000000000..1c3fced1f5 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java @@ -0,0 +1,42 @@ +package io.javaoperatorsdk.operator.api.monitoring; + +import java.util.Map; + +import io.javaoperatorsdk.operator.api.RetryInfo; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.Event; + +public interface Metrics { + Metrics NOOP = new Metrics() {}; + + default void receivedEvent(Event event) {} + + default void reconcileCustomResource(CustomResourceID customResourceID, + RetryInfo retryInfo) {} + + default void failedReconciliation(CustomResourceID customResourceID, + RuntimeException exception) {} + + default void cleanupDoneFor(CustomResourceID customResourceUid) {}; + + default void finishedReconciliation(CustomResourceID resourceID) {}; + + + interface ControllerExecution { + String name(); + + String controllerName(); + + String successTypeName(T result); + + T execute(); + } + + default T timeControllerExecution(ControllerExecution execution) { + return execution.execute(); + } + + default > T monitorSizeOf(T map, String name) { + return map; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index dd2c74354e..0f3bf2d9fd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -11,11 +11,11 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.CustomResourceUtils; -import io.javaoperatorsdk.operator.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 5c21747623..045b24b4c6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -16,6 +16,7 @@ import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; +import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; @@ -37,9 +38,6 @@ public class DefaultEventHandler> implements Even private static final Logger log = LoggerFactory.getLogger(DefaultEventHandler.class); - @Deprecated - private static EventMonitor monitor = EventMonitor.NOOP; - private final Set underProcessing = new HashSet<>(); private final EventDispatcher eventDispatcher; private final Retry retry; @@ -47,7 +45,7 @@ public class DefaultEventHandler> implements Even private final ExecutorService executor; private final String controllerName; private final ReentrantLock lock = new ReentrantLock(); - private final EventMonitor eventMonitor; + private final Metrics metrics; private volatile boolean running; private final ResourceCache resourceCache; private DefaultEventSourceManager eventSourceManager; @@ -60,7 +58,7 @@ public DefaultEventHandler(ConfiguredController controller, ResourceCache controller.getConfiguration().getName(), new EventDispatcher<>(controller), GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), - controller.getConfiguration().getConfigurationService().getMetrics().getEventMonitor(), + controller.getConfiguration().getConfigurationService().getMetrics(), new EventMarker()); } @@ -72,7 +70,7 @@ public DefaultEventHandler(ConfiguredController controller, ResourceCache private DefaultEventHandler(ResourceCache resourceCache, ExecutorService executor, String relatedControllerName, - EventDispatcher eventDispatcher, Retry retry, EventMonitor monitor, + EventDispatcher eventDispatcher, Retry retry, Metrics metrics, EventMarker eventMarker) { this.running = true; this.executor = @@ -84,7 +82,7 @@ private DefaultEventHandler(ResourceCache resourceCache, ExecutorService exec this.eventDispatcher = eventDispatcher; this.retry = retry; this.resourceCache = resourceCache; - this.eventMonitor = monitor != null ? monitor : EventMonitor.NOOP; + this.metrics = metrics != null ? metrics : Metrics.NOOP; this.eventMarker = eventMarker; } @@ -92,29 +90,6 @@ public void setEventSourceManager(DefaultEventSourceManager eventSourceManage this.eventSourceManager = eventSourceManager; } - /* - * TODO: promote this interface to top-level, probably create a `monitoring` package? - */ - public interface EventMonitor { - EventMonitor NOOP = new EventMonitor() { - @Override - public void processedEvent(CustomResourceID uid, Event event) {} - - @Override - public void failedEvent(CustomResourceID uid, Event event) {} - }; - - void processedEvent(CustomResourceID uid, Event event); - - void failedEvent(CustomResourceID uid, Event event); - } - - private EventMonitor monitor() { - // todo: remove us of static monitor, only here for backwards compatibility - return DefaultEventHandler.monitor != EventMonitor.NOOP ? DefaultEventHandler.monitor - : eventMonitor; - } - @Override public void handleEvent(Event event) { lock.lock(); @@ -124,21 +99,22 @@ public void handleEvent(Event event) { log.debug("Skipping event: {} because the event handler is shutting down", event); return; } - final var monitor = monitor(); - monitor.processedEvent(event.getRelatedCustomResourceID(), event); + final var resourceID = event.getRelatedCustomResourceID(); + metrics.receivedEvent(event); handleEventMarking(event); - if (!eventMarker.deleteEventPresent(event.getRelatedCustomResourceID())) { - submitReconciliationExecution(event.getRelatedCustomResourceID()); + if (!eventMarker.deleteEventPresent(resourceID)) { + submitReconciliationExecution(resourceID); } else { - cleanupForDeletedEvent(event.getRelatedCustomResourceID()); + cleanupForDeletedEvent(resourceID); } + } finally { lock.unlock(); } } - private boolean submitReconciliationExecution(CustomResourceID customResourceUid) { + private void submitReconciliationExecution(CustomResourceID customResourceUid) { boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); Optional latestCustomResource = resourceCache.getCustomResource(customResourceUid); @@ -146,14 +122,15 @@ private boolean submitReconciliationExecution(CustomResourceID customResourceUid if (!controllerUnderExecution && latestCustomResource.isPresent()) { setUnderExecutionProcessing(customResourceUid); - ExecutionScope executionScope = - new ExecutionScope( + final var retryInfo = retryInfo(customResourceUid); + ExecutionScope executionScope = + new ExecutionScope<>( latestCustomResource.get(), - retryInfo(customResourceUid)); + retryInfo); eventMarker.unMarkEventReceived(customResourceUid); + metrics.reconcileCustomResource(customResourceUid, retryInfo); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ControllerExecution(executionScope)); - return true; } else { log.debug( "Skipping executing controller for resource id: {}." @@ -165,7 +142,6 @@ private boolean submitReconciliationExecution(CustomResourceID customResourceUid log.warn("no custom resource found in cache for CustomResourceID: {}", customResourceUid); } - return false; } } @@ -201,13 +177,12 @@ void eventProcessingFinished( // Either way we don't want to retry. if (isRetryConfigured() && postExecutionControl.exceptionDuringExecution() && !eventMarker.deleteEventPresent(customResourceID)) { - handleRetryOnException(executionScope); - // todo revisit monitoring since events are not present anymore - // final var monitor = monitor(); executionScope.getEvents().forEach(e -> - // monitor.failedEvent(executionScope.getCustomResourceID(), e)); + handleRetryOnException(executionScope, + postExecutionControl.getRuntimeException().orElseThrow()); return; } cleanupOnSuccessfulExecution(executionScope); + metrics.finishedReconciliation(customResourceID); if (eventMarker.deleteEventPresent(customResourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { @@ -249,14 +224,11 @@ private boolean isCacheReadyForInstantReconciliation(ExecutionScope execution if (cachedCustomResourceVersion.equals(customResourceVersionAfterExecution)) { return true; } - if (cachedCustomResourceVersion.equals(originalResourceVersion)) { - return false; - } // If the cached resource version equals neither the version before nor after execution // probably an update happened on the custom resource independent of the framework during // reconciliation. We cannot tell at this point if it happened before our update or before. // (Well we could if we would parse resource version, but that should not be done by definition) - return true; + return !cachedCustomResourceVersion.equals(originalResourceVersion); } private void reScheduleExecutionIfInstructed(PostExecutionControl postExecutionControl, @@ -271,7 +243,8 @@ private void reScheduleExecutionIfInstructed(PostExecutionControl postExecuti * events (received meanwhile retry is in place or already in buffer) instantly or always wait * according to the retry timing if there was an exception. */ - private void handleRetryOnException(ExecutionScope executionScope) { + private void handleRetryOnException(ExecutionScope executionScope, + RuntimeException exception) { RetryExecution execution = getOrInitRetryExecution(executionScope); var customResourceID = executionScope.getCustomResourceID(); boolean eventPresent = eventMarker.eventPresent(customResourceID); @@ -291,6 +264,7 @@ private void handleRetryOnException(ExecutionScope executionScope) { "Scheduling timer event for retry with delay:{} for resource: {}", delay, customResourceID); + metrics.failedReconciliation(customResourceID, exception); eventSourceManager .getRetryAndRescheduleTimerEventSource() .scheduleOnce(executionScope.getCustomResource(), delay); @@ -322,6 +296,7 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) private void cleanupForDeletedEvent(CustomResourceID customResourceUid) { eventSourceManager.cleanupForCustomResource(customResourceUid); eventMarker.cleanup(customResourceUid); + metrics.cleanupDoneFor(customResourceUid); } private boolean isControllerUnderExecution(CustomResourceID customResourceUid) { @@ -360,10 +335,17 @@ private ControllerExecution(ExecutionScope executionScope) { @Override public void run() { // change thread name for easier debugging - Thread.currentThread().setName("EventHandler-" + controllerName); - PostExecutionControl postExecutionControl = - eventDispatcher.handleExecution(executionScope); - eventProcessingFinished(executionScope, postExecutionControl); + final var thread = Thread.currentThread(); + final var name = thread.getName(); + try { + thread.setName("EventHandler-" + controllerName); + PostExecutionControl postExecutionControl = + eventDispatcher.handleExecution(executionScope); + eventProcessingFinished(executionScope, postExecutionControl); + } finally { + // restore original name + thread.setName(name); + } } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index c3ab127a50..5d900ab5cc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -24,7 +24,7 @@ public class DefaultEventSourceManager> private final Set eventSources = Collections.synchronizedSet(new HashSet<>()); private DefaultEventHandler defaultEventHandler; private TimerEventSource retryAndRescheduleTimerEventSource; - private CustomResourceEventSource customResourceEventSource; + private CustomResourceEventSource customResourceEventSource; DefaultEventSourceManager(DefaultEventHandler defaultEventHandler) { init(defaultEventHandler); @@ -58,7 +58,7 @@ public void close() { try { eventSource.close(); } catch (Exception e) { - log.warn("Error closing {} -> {}", eventSource); + log.warn("Error closing {} -> {}", eventSource, e); } } eventSources.clear(); @@ -99,7 +99,7 @@ public void cleanupForCustomResource(CustomResourceID customResourceUid) { } } - public TimerEventSource getRetryAndRescheduleTimerEventSource() { + public TimerEventSource getRetryAndRescheduleTimerEventSource() { return retryAndRescheduleTimerEventSource; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index b709c026e1..da32b467e0 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -10,7 +10,6 @@ import org.mockito.ArgumentMatchers; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.Context; import io.javaoperatorsdk.operator.api.DeleteControl; @@ -19,6 +18,7 @@ import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index a24c32a547..b8e4381f3b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -9,10 +9,10 @@ import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.Metrics; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.EventHandler; From fed1261589d9718ca348e5a2a6f25fc126023606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 27 Oct 2021 09:47:25 +0200 Subject: [PATCH 0107/1608] feat: removing Event using DefaultEvent instead, renamed DefaultEvent to Event. - EventHandler is not closeable --- .../processing/DefaultEventHandler.java | 9 ++++---- .../processing/event/DefaultEvent.java | 23 ------------------- .../operator/processing/event/Event.java | 18 +++++++++++++-- .../processing/event/EventHandler.java | 7 +----- .../event/internal/CustomResourceEvent.java | 4 ++-- .../internal/CustomResourceEventSource.java | 1 - .../event/internal/InformerEventSource.java | 4 ++-- .../event/internal/TimerEventSource.java | 4 ++-- .../processing/DefaultEventHandlerTest.java | 3 +-- 9 files changed, 28 insertions(+), 45 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 045b24b4c6..aac79938cd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing; +import java.io.Closeable; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -17,10 +18,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.*; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -34,7 +32,8 @@ * Event handler that makes sure that events are processed in a "single threaded" way per resource * UID, while buffering events which are received during an execution. */ -public class DefaultEventHandler> implements EventHandler { +public class DefaultEventHandler> + implements EventHandler, Closeable { private static final Logger log = LoggerFactory.getLogger(DefaultEventHandler.class); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java deleted file mode 100644 index 7c55939da5..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event; - -@SuppressWarnings("rawtypes") -public class DefaultEvent implements Event { - - private final CustomResourceID relatedCustomResource; - - public DefaultEvent(CustomResourceID targetCustomResource) { - this.relatedCustomResource = targetCustomResource; - } - - @Override - public CustomResourceID getRelatedCustomResourceID() { - return relatedCustomResource; - } - - @Override - public String toString() { - return "DefaultEvent{" + - "relatedCustomResource=" + relatedCustomResource + - '}'; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java index b0d51a37c0..ff871b1534 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java @@ -1,7 +1,21 @@ package io.javaoperatorsdk.operator.processing.event; -public interface Event { +public class Event { - CustomResourceID getRelatedCustomResourceID(); + private final CustomResourceID relatedCustomResource; + public Event(CustomResourceID targetCustomResource) { + this.relatedCustomResource = targetCustomResource; + } + + public CustomResourceID getRelatedCustomResourceID() { + return relatedCustomResource; + } + + @Override + public String toString() { + return "DefaultEvent{" + + "relatedCustomResource=" + relatedCustomResource + + '}'; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventHandler.java index e0a657e1d1..73f7867da4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventHandler.java @@ -1,12 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; -import java.io.Closeable; -import java.io.IOException; - -public interface EventHandler extends Closeable { +public interface EventHandler { void handleEvent(Event event); - @Override - default void close() throws IOException {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java index 0c20369e0a..9752e7cf5e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java @@ -1,9 +1,9 @@ package io.javaoperatorsdk.operator.processing.event.internal; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEvent; +import io.javaoperatorsdk.operator.processing.event.Event; -public class CustomResourceEvent extends DefaultEvent { +public class CustomResourceEvent extends Event { private final ResourceAction action; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index b9c4c5b6a3..84a3ad75dc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -106,7 +106,6 @@ public void start() { @Override public void close() throws IOException { - eventHandler.close(); for (SharedIndexInformer informer : sharedIndexInformers.values()) { try { log.info("Closing informer {} -> {}", controller, informer); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 42b1c94084..9d32432360 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -13,7 +13,7 @@ import io.fabric8.kubernetes.client.informers.cache.Store; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEvent; +import io.javaoperatorsdk.operator.processing.event.Event; public class InformerEventSource extends AbstractEventSource { @@ -82,7 +82,7 @@ private void propagateEvent(T object) { return; } uids.forEach(uid -> { - DefaultEvent event = new DefaultEvent(CustomResourceID.fromResource(object)); + Event event = new Event(CustomResourceID.fromResource(object)); this.eventHandler.handleEvent(event); }); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index 5015df4281..e8a1b7bc20 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -13,7 +13,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEvent; +import io.javaoperatorsdk.operator.processing.event.Event; public class TimerEventSource> extends AbstractEventSource { private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class); @@ -95,7 +95,7 @@ public EventProducerTimeTask(CustomResourceID customResourceUid) { public void run() { if (running.get()) { log.debug("Producing event for custom resource id: {}", customResourceUid); - eventHandler.handleEvent(new DefaultEvent(customResourceUid)); + eventHandler.handleEvent(new Event(customResourceUid)); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index f231852537..93a36d5304 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -13,7 +13,6 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEvent; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; @@ -315,7 +314,7 @@ private CustomResourceEvent prepareCREvent(CustomResourceID uid) { } private Event nonCREvent(CustomResourceID relatedCustomResourceUid) { - return new DefaultEvent(relatedCustomResourceUid); + return new Event(relatedCustomResourceUid); } private void overrideData(CustomResourceID id, CustomResource applyTo) { From c5c83afa4f983550549c7e774c4ead03fd173194 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 26 Oct 2021 19:19:39 +0200 Subject: [PATCH 0108/1608] fix: prevent double registration of same CR with different controllers Fixes #626 --- .../io/javaoperatorsdk/operator/Operator.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) 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 31bf1a80ec..5b23e3817a 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 @@ -3,9 +3,10 @@ import java.io.Closeable; import java.io.IOException; import java.net.ConnectException; -import java.util.Collections; -import java.util.LinkedList; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,7 +51,7 @@ public ConfigurationService getConfigurationService() { } public List getControllers() { - return Collections.unmodifiableList(controllers.controllers); + return new ArrayList<>(controllers.controllers.values()); } /** @@ -159,7 +160,7 @@ public void register( } private static class ControllerManager implements Closeable { - private final List controllers = new LinkedList<>(); + private final Map controllers = new HashMap<>(); private boolean started = false; @@ -173,7 +174,7 @@ public synchronized void shouldStart() { } public synchronized void start() { - controllers.parallelStream().forEach(ConfiguredController::start); + controllers.values().parallelStream().forEach(ConfiguredController::start); started = true; } @@ -183,7 +184,7 @@ public synchronized void close() { return; } - this.controllers.parallelStream().forEach(closeable -> { + this.controllers.values().parallelStream().forEach(closeable -> { try { log.debug("closing {}", closeable); closeable.close(); @@ -196,7 +197,15 @@ public synchronized void close() { } public synchronized void add(ConfiguredController configuredController) { - this.controllers.add(configuredController); + final var configuration = configuredController.getConfiguration(); + final var crdName = configuration.getCRDName(); + final var existing = controllers.get(crdName); + if (existing != null) { + throw new OperatorException("Cannot register controller " + configuration.getName() + + ": another controller (" + existing.getConfiguration().getName() + + ") is already registered for CRD " + crdName); + } + this.controllers.put(crdName, configuredController); if (started) { configuredController.start(); } From 26cfdeee78aa5f6984d12ae39e4804b1649387ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 28 Oct 2021 08:42:24 +0200 Subject: [PATCH 0109/1608] feat: Observed generation in status support (#628) --- .../operator/api/ObservedGenerationAware.java | 28 ++++++++ .../api/ObservedGenerationAwareStatus.java | 21 ++++++ .../operator/processing/EventDispatcher.java | 22 +++++-- .../internal/CustomResourceEventFilters.java | 16 ++++- .../processing/EventDispatcherTest.java | 29 +++++++++ .../CustomResourceEventFilterTest.java | 64 +++++++++++++++++-- .../ObservedGenCustomResource.java | 21 ++++++ .../observedgeneration/ObservedGenSpec.java | 21 ++++++ .../observedgeneration/ObservedGenStatus.java | 7 ++ 9 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenCustomResource.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenSpec.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenStatus.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java new file mode 100644 index 0000000000..946fde48a3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.api; + +import java.util.Optional; + +import io.fabric8.kubernetes.client.CustomResource; + +/** + * If the custom resource's status implements this interface, the observed generation will be + * automatically handled. The last observed generation will be updated on status when the status is + * instructed to be updated (see below). In addition to that, controller configuration will be + * checked if is set to generation aware. If generation aware config is turned off, this interface + * is ignored. + * + * In order to work the status object returned by CustomResource.getStatus() should not be null. In + * addition to that from the controller that the + * {@link UpdateControl#updateStatusSubResource(CustomResource)} or + * {@link UpdateControl#updateCustomResourceAndStatus(CustomResource)} should be returned. The + * observed generation is not updated in other cases. + * + * @see ObservedGenerationAwareStatus + */ +public interface ObservedGenerationAware { + + void setObservedGeneration(Long generation); + + Optional getObservedGeneration(); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java new file mode 100644 index 0000000000..843736242b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.api; + +import java.util.Optional; + +/** + * A helper base class for status sub-resources classes to extend to support generate awareness. + */ +public class ObservedGenerationAwareStatus implements ObservedGenerationAware { + + private Long observedGeneration; + + @Override + public void setObservedGeneration(Long generation) { + this.observedGeneration = generation; + } + + @Override + public Optional getObservedGeneration() { + return Optional.ofNullable(observedGeneration); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index dd8ae8dbae..3000e463b6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -117,11 +117,9 @@ private PostExecutionControl handleCreateOrUpdate( .getCustomResource() .getMetadata() .setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion()); - updatedCustomResource = - customResourceFacade.updateStatus(updateControl.getCustomResource()); + updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); } else if (updateControl.isUpdateStatusSubResource()) { - updatedCustomResource = - customResourceFacade.updateStatus(updateControl.getCustomResource()); + updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); } else if (updateControl.isUpdateCustomResource()) { updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); } @@ -129,6 +127,22 @@ private PostExecutionControl handleCreateOrUpdate( } } + private R updateStatusGenerationAware(R customResource) { + updateStatusObservedGenerationIfRequired(customResource); + return customResourceFacade.updateStatus(customResource); + } + + private void updateStatusObservedGenerationIfRequired(R customResource) { + if (controller.getConfiguration().isGenerationAware()) { + var status = customResource.getStatus(); + // Note that if status is null we won't update the observed generation. + if (status instanceof ObservedGenerationAware) { + ((ObservedGenerationAware) status) + .setObservedGeneration(customResource.getMetadata().getGeneration()); + } + } + } + private PostExecutionControl createPostExecutionControl(R updatedCustomResource, UpdateControl updateControl) { PostExecutionControl postExecutionControl; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java index 2f3bc72327..7de41c455d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.processing.event.internal; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.ObservedGenerationAware; /** * Convenience implementations of, and utility methods for, {@link CustomResourceEventFilter}. @@ -21,9 +22,18 @@ public final class CustomResourceEventFilters { }; private static final CustomResourceEventFilter GENERATION_AWARE = - (configuration, oldResource, newResource) -> oldResource == null - || !configuration.isGenerationAware() - || oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); + (configuration, oldResource, newResource) -> { + final var status = newResource.getStatus(); + final var generationAware = configuration.isGenerationAware(); + if (generationAware && status instanceof ObservedGenerationAware) { + var actualGeneration = newResource.getMetadata().getGeneration(); + var observedGeneration = ((ObservedGenerationAware) status) + .getObservedGeneration(); + return observedGeneration.map(aLong -> actualGeneration > aLong).orElse(true); + } + return oldResource == null || !generationAware || + oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); + }; private static final CustomResourceEventFilter PASSTHROUGH = (configuration, oldResource, newResource) -> true; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index da32b467e0..87385e61d2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -9,6 +9,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.Context; @@ -23,6 +24,7 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; +import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.ADDED; import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.UPDATED; @@ -320,6 +322,33 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); } + @Test + void setObservedGenerationForStatusIfNeeded() { + var observedGenResource = createObservedGenCustomResource(); + + when(configuration.isGenerationAware()).thenReturn(true); + when(controller.createOrUpdateResource(eq(observedGenResource), any())) + .thenReturn( + UpdateControl.updateStatusSubResource(observedGenResource)); + + when(customResourceFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource); + + PostExecutionControl control = eventDispatcher.handleExecution( + executionScopeWithCREvent(ADDED, observedGenResource)); + assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration().get()) + .isEqualTo(1L); + } + + @Test + private ObservedGenCustomResource createObservedGenCustomResource() { + ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); + observedGenCustomResource.setMetadata(new ObjectMeta()); + observedGenCustomResource.getMetadata().setGeneration(1L); + observedGenCustomResource.getMetadata().setFinalizers(new ArrayList<>()); + observedGenCustomResource.getMetadata().getFinalizers().add(DEFAULT_FINALIZER); + return observedGenCustomResource; + } + private void markForDeletion(CustomResource customResource) { customResource.getMetadata().setDeletionTimestamp("2019-8-10"); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java index 3e9e413f1e..86081e4af1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; @@ -15,6 +17,7 @@ import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.Mockito.any; @@ -94,6 +97,31 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { verify(eventHandler, times(1)).handleEvent(any()); } + @Test + public void observedGenerationFiltering() { + var config = new ObservedGenControllerConfig(FINALIZER, true, null); + when(config.getConfigurationService().getResourceCloner()) + .thenReturn(ConfigurationService.DEFAULT_CLONER); + + var controller = new ObservedGenConfiguredController(config); + var eventSource = new CustomResourceEventSource<>(controller); + eventSource.setEventHandler(eventHandler); + + ObservedGenCustomResource cr = new ObservedGenCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setFinalizers(List.of(FINALIZER)); + cr.getMetadata().setGeneration(5L); + cr.getStatus().setObservedGeneration(5L); + + eventSource.eventReceived(ResourceAction.UPDATED, cr, null); + verify(eventHandler, times(0)).handleEvent(any()); + + cr.getMetadata().setGeneration(6L); + + eventSource.eventReceived(ResourceAction.UPDATED, cr, null); + verify(eventHandler, times(1)).handleEvent(any()); + } + @Test public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { var config = new TestControllerConfig( @@ -124,11 +152,25 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { verify(eventHandler, times(2)).handleEvent(any()); } - private static class TestControllerConfig extends - DefaultControllerConfiguration { - + private static class TestControllerConfig extends ControllerConfig { public TestControllerConfig(String finalizer, boolean generationAware, CustomResourceEventFilter eventFilter) { + super(finalizer, generationAware, eventFilter, TestCustomResource.class); + } + } + private static class ObservedGenControllerConfig + extends ControllerConfig { + public ObservedGenControllerConfig(String finalizer, boolean generationAware, + CustomResourceEventFilter eventFilter) { + super(finalizer, generationAware, eventFilter, ObservedGenCustomResource.class); + } + } + + private static class ControllerConfig> extends + DefaultControllerConfiguration { + + public ControllerConfig(String finalizer, boolean generationAware, + CustomResourceEventFilter eventFilter, Class customResourceClass) { super( null, null, @@ -139,7 +181,7 @@ public TestControllerConfig(String finalizer, boolean generationAware, null, null, eventFilter, - TestCustomResource.class, + customResourceClass, mock(ConfigurationService.class)); when(getConfigurationService().getResourceCloner()) @@ -158,4 +200,18 @@ public MixedOperation { + + public ObservedGenConfiguredController( + ControllerConfiguration configuration) { + super(null, configuration, null); + } + + @Override + public MixedOperation, Resource> getCRClient() { + return mock(MixedOperation.class); + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenCustomResource.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenCustomResource.java new file mode 100644 index 0000000000..74f58b795f --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenCustomResource.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk.io") +@Version("v1") +public class ObservedGenCustomResource + extends CustomResource { + + @Override + protected ObservedGenSpec initSpec() { + return new ObservedGenSpec(); + } + + @Override + protected ObservedGenStatus initStatus() { + return new ObservedGenStatus(); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenSpec.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenSpec.java new file mode 100644 index 0000000000..181204bc1c --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenSpec.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +public class ObservedGenSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "TestCustomResourceSpec{" + + "value='" + value + '\'' + + '}'; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenStatus.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenStatus.java new file mode 100644 index 0000000000..d4ffee5416 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenStatus.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class ObservedGenStatus extends ObservedGenerationAwareStatus { + +} From cbfd9930afdeeab373310c4b3fa9888cec95eabe Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 28 Oct 2021 12:50:49 +0200 Subject: [PATCH 0110/1608] refactor!: replace Closeable by explicit LifecycleAware interface (#633) This ensures that stoppable classes can also be started if needed. Fixes #629 --- .../io/javaoperatorsdk/operator/Operator.java | 31 +++++++++---------- .../operator/api/LifecycleAware.java | 9 ++++++ .../api/config/ExecutorServiceManager.java | 6 ++-- .../processing/ConfiguredController.java | 16 +++++----- .../processing/DefaultEventHandler.java | 22 ++++++++++--- .../event/DefaultEventSourceManager.java | 20 ++++++++---- .../processing/event/EventSource.java | 18 ++--------- .../internal/CustomResourceEventSource.java | 9 +++--- .../event/internal/InformerEventSource.java | 3 +- .../event/internal/TimerEventSource.java | 3 +- .../processing/DefaultEventHandlerTest.java | 2 +- .../event/DefaultEventSourceManagerTest.java | 6 ++-- .../event/internal/TimerEventSourceTest.java | 4 +-- 13 files changed, 82 insertions(+), 67 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/LifecycleAware.java 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 5b23e3817a..c63dd6c972 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 @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator; -import java.io.Closeable; -import java.io.IOException; import java.net.ConnectException; import java.util.ArrayList; import java.util.HashMap; @@ -15,6 +13,7 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Version; +import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -22,7 +21,7 @@ import io.javaoperatorsdk.operator.processing.ConfiguredController; @SuppressWarnings("rawtypes") -public class Operator implements AutoCloseable { +public class Operator implements AutoCloseable, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient kubernetesClient; private final ConfigurationService configurationService; @@ -90,18 +89,23 @@ public void start() { controllers.start(); } - /** Stop the operator. */ @Override - public void close() { + public void stop() throws OperatorException { log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); - controllers.close(); + controllers.stop(); - ExecutorServiceManager.close(); + ExecutorServiceManager.stop(); kubernetesClient.close(); } + /** Stop the operator. */ + @Override + public void close() { + stop(); + } + /** * Add a registration requests for the specified controller with this operator. The effective * registration of the controller is delayed till the operator is started. @@ -159,7 +163,7 @@ public void register( } } - private static class ControllerManager implements Closeable { + private static class ControllerManager implements LifecycleAware { private final Map controllers = new HashMap<>(); private boolean started = false; @@ -178,19 +182,14 @@ public synchronized void start() { started = true; } - @Override - public synchronized void close() { + public synchronized void stop() { if (!started) { return; } this.controllers.values().parallelStream().forEach(closeable -> { - try { - log.debug("closing {}", closeable); - closeable.close(); - } catch (IOException e) { - log.warn("Error closing {}", closeable, e); - } + log.debug("closing {}", closeable); + closeable.stop(); }); started = false; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/LifecycleAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/LifecycleAware.java new file mode 100644 index 0000000000..320368bcea --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/LifecycleAware.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api; + +import io.javaoperatorsdk.operator.OperatorException; + +public interface LifecycleAware { + void start() throws OperatorException; + + void stop() throws OperatorException; +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index 682b004c3d..62d44b66a2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -34,9 +34,9 @@ public static void init(ConfigurationService configuration) { } } - public static void close() { + public static void stop() { if (instance != null) { - instance.stop(); + instance.doStop(); } // make sure that we remove the singleton so that the thread pool is re-created on next call to // start @@ -55,7 +55,7 @@ public ExecutorService executorService() { return executor; } - private void stop() { + private void doStop() { try { log.debug("Closing executor"); executor.shutdown(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index 0f3bf2d9fd..7249dfe4aa 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.processing; -import java.io.Closeable; -import java.io.IOException; import java.util.Objects; import io.fabric8.kubernetes.api.model.KubernetesResourceList; @@ -13,14 +11,19 @@ import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.DeleteControl; +import io.javaoperatorsdk.operator.api.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.LifecycleAware; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; public class ConfiguredController> implements ResourceController, - Closeable, EventSourceInitializer { + LifecycleAware, EventSourceInitializer { private final ResourceController controller; private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; @@ -214,10 +217,9 @@ public EventSourceManager getEventSourceManager() { return eventSourceManager; } - @Override - public void close() throws IOException { + public void stop() { if (eventSourceManager != null) { - eventSourceManager.close(); + eventSourceManager.stop(); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index aac79938cd..212cd378d7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing; -import java.io.Closeable; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -14,11 +13,16 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.event.*; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -33,7 +37,7 @@ * UID, while buffering events which are received during an execution. */ public class DefaultEventHandler> - implements EventHandler, Closeable { + implements EventHandler, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(DefaultEventHandler.class); @@ -315,7 +319,7 @@ private boolean isRetryConfigured() { } @Override - public void close() { + public void stop() { lock.lock(); try { this.running = false; @@ -324,6 +328,16 @@ public void close() { } } + @Override + public void start() throws OperatorException { + lock.lock(); + try { + this.running = true; + } finally { + lock.unlock(); + } + } + private class ControllerExecution implements Runnable { private final ExecutionScope executionScope; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java index 5d900ab5cc..dd8e7f19ce 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java @@ -1,7 +1,9 @@ package io.javaoperatorsdk.operator.processing.event; -import java.io.Closeable; -import java.util.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; @@ -10,13 +12,14 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.processing.ConfiguredController; import io.javaoperatorsdk.operator.processing.DefaultEventHandler; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; public class DefaultEventSourceManager> - implements EventSourceManager, Closeable { + implements EventSourceManager, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(DefaultEventSourceManager.class); @@ -45,18 +48,23 @@ private void init(DefaultEventHandler defaultEventHandler) { } @Override - public void close() { + public void start() throws OperatorException { + defaultEventHandler.start(); + } + + @Override + public void stop() { lock.lock(); try { try { - defaultEventHandler.close(); + defaultEventHandler.stop(); } catch (Exception e) { log.warn("Error closing event handler", e); } log.debug("Closing event sources."); for (var eventSource : eventSources) { try { - eventSource.close(); + eventSource.stop(); } catch (Exception e) { log.warn("Error closing {} -> {}", eventSource, e); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java index 22187ddb86..7646dcc353 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java @@ -1,22 +1,8 @@ package io.javaoperatorsdk.operator.processing.event; -import java.io.Closeable; -import java.io.IOException; +import io.javaoperatorsdk.operator.api.LifecycleAware; -public interface EventSource extends Closeable { - - /** - * This method is invoked when this {@link EventSource} instance is properly registered to a - * {@link EventSourceManager}. - */ - default void start() {} - - /** - * This method is invoked when this {@link EventSource} instance is de-registered from a - * {@link EventSourceManager}. - */ - @Override - default void close() throws IOException {} +public interface EventSource extends LifecycleAware { void setEventHandler(EventHandler eventHandler); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 84a3ad75dc..a9a80c10f3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -105,13 +104,13 @@ public void start() { } @Override - public void close() throws IOException { + public void stop() { for (SharedIndexInformer informer : sharedIndexInformers.values()) { try { - log.info("Closing informer {} -> {}", controller, informer); - informer.close(); + log.info("Stopping informer {} -> {}", controller, informer); + informer.stop(); } catch (Exception e) { - log.warn("Error closing informer {} -> {}", controller, informer, e); + log.warn("Error stopping informer {} -> {}", controller, informer, e); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 9d32432360..d9252bb4c6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import java.io.IOException; import java.util.Objects; import java.util.Set; import java.util.function.Function; @@ -93,7 +92,7 @@ public void start() { } @Override - public void close() throws IOException { + public void stop() { sharedInformer.close(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index e8a1b7bc20..4fc78ebb44 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import java.io.IOException; import java.util.Map; import java.util.Timer; import java.util.TimerTask; @@ -76,7 +75,7 @@ public void start() { } @Override - public void close() throws IOException { + public void stop() { running.set(false); onceTasks.keySet().forEach(this::cancelOnceSchedule); timerTasks.keySet().forEach(this::cancelSchedule); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 93a36d5304..2ffabd6fe1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -194,7 +194,7 @@ public void reScheduleOnlyIfNotExecutedBufferedEvents() { @Test public void doNotFireEventsIfClosing() { - defaultEventHandler.close(); + defaultEventHandler.stop(); defaultEventHandler.handleEvent(prepareCREvent()); verify(eventDispatcherMock, timeout(50).times(0)).handleExecution(any()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java index ad87000a52..d5b87bb5b5 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java @@ -42,10 +42,10 @@ public void closeShouldCascadeToEventSources() throws IOException { defaultEventSourceManager.registerEventSource(eventSource); defaultEventSourceManager.registerEventSource(eventSource2); - defaultEventSourceManager.close(); + defaultEventSourceManager.stop(); - verify(eventSource, times(1)).close(); - verify(eventSource2, times(1)).close(); + verify(eventSource, times(1)).stop(); + verify(eventSource2, times(1)).stop(); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index 0d9c3b5a11..34bc6b2f92 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -112,7 +112,7 @@ public void deRegistersOnceEventSources() { public void eventNotRegisteredIfStopped() throws IOException { TestCustomResource customResource = TestUtils.testCustomResource(); - timerEventSource.close(); + timerEventSource.stop(); assertThatExceptionOfType(IllegalStateException.class).isThrownBy( () -> timerEventSource.scheduleOnce(customResource, PERIOD)); } @@ -120,7 +120,7 @@ public void eventNotRegisteredIfStopped() throws IOException { @Test public void eventNotFiredIfStopped() throws IOException { timerEventSource.scheduleOnce(TestUtils.testCustomResource(), PERIOD); - timerEventSource.close(); + timerEventSource.stop(); untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } From 7fec74081c2999e40fac6c336c6a6efdf8b30a55 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 28 Oct 2021 13:44:38 +0200 Subject: [PATCH 0111/1608] refactor: fix generics and minor clean-ups (#636) --- .../operator/ControllerUtils.java | 1 + .../io/javaoperatorsdk/operator/Operator.java | 6 +- .../operator/api/BaseControl.java | 2 +- .../javaoperatorsdk/operator/api/Context.java | 4 +- .../operator/api/DefaultContext.java | 4 +- .../operator/api/DeleteControl.java | 2 +- .../operator/api/ResourceController.java | 6 +- .../operator/api/UpdateControl.java | 7 +- .../config/AbstractConfigurationService.java | 15 +- .../api/config/ConfigurationService.java | 4 +- .../config/ConfigurationServiceOverrider.java | 2 +- .../api/config/ControllerConfiguration.java | 2 +- .../ControllerConfigurationOverrider.java | 2 +- .../operator/api/monitoring/Metrics.java | 4 +- .../processing/ConfiguredController.java | 14 +- .../operator/processing/EventDispatcher.java | 16 +- .../internal/CustomResourceEventFilter.java | 2 +- .../internal/CustomResourceEventFilters.java | 20 +-- .../operator/api/DeleteControlTest.java | 5 +- .../processing/EventDispatcherTest.java | 139 ++++++++---------- .../internal/CustomResourceSelectorTest.java | 2 +- .../simple/TestCustomResourceController.java | 4 +- operator-framework-junit5/pom.xml | 2 - .../runtime/AnnotationConfiguration.java | 2 +- .../config/runtime/ClassMappingProvider.java | 3 +- .../runtime/DefaultConfigurationService.java | 4 +- .../runtime/RuntimeControllerMetadata.java | 4 +- .../config/runtime/TypeParameterResolver.java | 2 +- .../DefaultConfigurationServiceTest.java | 8 +- .../config/runtime/TestCustomResource.java | 2 +- ...bleUpdateTestCustomResourceController.java | 7 +- ...entSourceTestCustomResourceController.java | 8 +- ...entSourceTestCustomResourceController.java | 8 +- .../RetryTestCustomResourceController.java | 2 +- .../simple/TestCustomResourceController.java | 4 +- ...bResourceTestCustomResourceController.java | 2 +- .../compile-fixtures/AbstractController.java | 4 +- .../AdditionalControllerInterface.java | 2 +- .../ControllerImplemented2Interfaces.java | 6 +- ...rImplementedIntermediateAbstractClass.java | 4 +- .../MultilevelAbstractController.java | 2 +- .../MultilevelController.java | 6 +- pom.xml | 1 + .../sample/CustomServiceController.java | 4 +- 44 files changed, 174 insertions(+), 176 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java index 023b333758..b525055a81 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java @@ -5,6 +5,7 @@ import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.api.ResourceController; +@SuppressWarnings("rawtypes") public class ControllerUtils { private static final String FINALIZER_NAME_SUFFIX = "/finalizer"; 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 c63dd6c972..ae094b25c6 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 @@ -114,7 +114,7 @@ public void close() { * @param the {@code CustomResource} type associated with the controller * @throws OperatorException if a problem occurred during the registration process */ - public void register(ResourceController controller) + public > void register(ResourceController controller) throws OperatorException { register(controller, null); } @@ -132,7 +132,7 @@ public void register(ResourceController controller * @param the {@code CustomResource} type associated with the controller * @throws OperatorException if a problem occurred during the registration process */ - public void register( + public > void register( ResourceController controller, ControllerConfiguration configuration) throws OperatorException { final var existing = configurationService.getConfigurationFor(controller); @@ -148,7 +148,7 @@ public void register( configuration = existing; } final var configuredController = - new ConfiguredController(controller, configuration, kubernetesClient); + new ConfiguredController<>(controller, configuration, kubernetesClient); controllers.add(configuredController); final var watchedNS = diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java index 327bc34791..aeca177cae 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java @@ -3,7 +3,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -public abstract class BaseControl { +public abstract class BaseControl> { private Long scheduleDelay = null; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java index cc832a2e21..3651414c16 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java @@ -2,9 +2,7 @@ import java.util.Optional; -import io.fabric8.kubernetes.client.CustomResource; - -public interface Context { +public interface Context { Optional getRetryInfo(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java index f32a784e11..b74793d25c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java @@ -2,9 +2,7 @@ import java.util.Optional; -import io.fabric8.kubernetes.client.CustomResource; - -public class DefaultContext implements Context { +public class DefaultContext implements Context { private final RetryInfo retryInfo; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java index b1ddac3df3..0c2c3c87e5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java @@ -22,7 +22,7 @@ public boolean isRemoveFinalizer() { @Override public DeleteControl rescheduleAfter(long delay) { - if (removeFinalizer == true) { + if (removeFinalizer) { throw new IllegalStateException("Cannot reschedule deleteResource if removing finalizer"); } return super.rescheduleAfter(delay); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java index 817cb554fc..7fe27ec908 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java @@ -2,7 +2,7 @@ import io.fabric8.kubernetes.client.CustomResource; -public interface ResourceController { +public interface ResourceController> { /** * Note that this method is used in combination of finalizers. If automatic finalizer handling is @@ -28,7 +28,7 @@ public interface ResourceController { * finalizer to indicate that the resource should not be deleted after all, in which case * the controller should restore the resource's state appropriately. */ - default DeleteControl deleteResource(R resource, Context context) { + default DeleteControl deleteResource(R resource, Context context) { return DeleteControl.defaultDelete(); } @@ -46,6 +46,6 @@ default DeleteControl deleteResource(R resource, Context context) { * be skipped. However we will always call an update if there is no finalizer on object * and it's not marked for deletion. */ - UpdateControl createOrUpdateResource(R resource, Context context); + UpdateControl createOrUpdateResource(R resource, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java index 18cc2ca4ee..2c9531ca06 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java @@ -2,6 +2,7 @@ import io.fabric8.kubernetes.client.CustomResource; +@SuppressWarnings("rawtypes") public class UpdateControl extends BaseControl> { private final T customResource; @@ -30,16 +31,16 @@ public static UpdateControl updateStatusSubResourc /** * As a results of this there will be two call to K8S API. First the custom resource will be * updates then the status sub-resource. - * + * * @param customResource - custom resource to use in both API calls * @return UpdateControl instance */ - public static UpdateControl updateCustomResourceAndStatus( + public static > UpdateControl updateCustomResourceAndStatus( T customResource) { return new UpdateControl<>(customResource, true, true); } - public static UpdateControl noUpdate() { + public static > UpdateControl noUpdate() { return new UpdateControl<>(null, false, false); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index 1a37986d52..af9c7856b8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -9,6 +9,7 @@ import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.api.ResourceController; +@SuppressWarnings("rawtypes") public class AbstractConfigurationService implements ConfigurationService { private final Map configurations = new ConcurrentHashMap<>(); private final Version version; @@ -17,15 +18,15 @@ public AbstractConfigurationService(Version version) { this.version = version; } - protected void register(ControllerConfiguration config) { + protected > void register(ControllerConfiguration config) { put(config, true); } - protected void replace(ControllerConfiguration config) { + protected > void replace(ControllerConfiguration config) { put(config, false); } - private void put( + private > void put( ControllerConfiguration config, boolean failIfExisting) { final var name = config.getName(); if (failIfExisting) { @@ -38,8 +39,8 @@ private void put( config.setConfigurationService(this); } - protected void throwExceptionOnNameCollision( - String newControllerClassName, ControllerConfiguration existing) { + protected > void throwExceptionOnNameCollision( + String newControllerClassName, ControllerConfiguration existing) { throw new IllegalArgumentException( "Controller name '" + existing.getName() @@ -50,7 +51,7 @@ protected void throwExceptionOnNameCollision( } @Override - public ControllerConfiguration getConfigurationFor( + public > ControllerConfiguration getConfigurationFor( ResourceController controller) { final var key = keyFor(controller); final var configuration = configurations.get(key); @@ -72,7 +73,7 @@ private String getControllersNameMessage() { + "."; } - protected String keyFor(ResourceController controller) { + protected > String keyFor(ResourceController controller) { return ControllerUtils.getNameFor(controller); } 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 fb53f7eaae..b65115ec56 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 @@ -34,9 +34,9 @@ public interface ConfigurationService { * @param controller the controller we want the configuration of * @param the {@code CustomResource} type associated with the specified controller * @return the {@link ControllerConfiguration} associated with the specified controller or {@code - * null} if no configuration exists for the controller + * null} if no configuration exists for the controller */ - ControllerConfiguration getConfigurationFor( + > ControllerConfiguration getConfigurationFor( ResourceController controller); /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 2cecfb552d..62630f3ce2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -61,7 +61,7 @@ public ConfigurationServiceOverrider withMetrics(Metrics metrics) { public ConfigurationService build() { return new ConfigurationService() { @Override - public ControllerConfiguration getConfigurationFor( + public > ControllerConfiguration getConfigurationFor( ResourceController controller) { return original.getConfigurationFor(controller); } 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 a1d097daba..65ec26964b 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 @@ -10,7 +10,7 @@ import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters; -public interface ControllerConfiguration { +public interface ControllerConfiguration> { default String getName() { return ControllerUtils.getDefaultResourceControllerName(getAssociatedControllerClassName()); 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 f85870ac76..b9e4f9b7e6 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 @@ -48,7 +48,7 @@ public ControllerConfigurationOverrider addingNamespaces(String... namespaces } public ControllerConfigurationOverrider removingNamespaces(String... namespaces) { - this.namespaces.removeAll(List.of(namespaces)); + List.of(namespaces).forEach(this.namespaces::remove); return this; } 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 1c3fced1f5..5544bc542e 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 @@ -17,9 +17,9 @@ default void reconcileCustomResource(CustomResourceID customResourceID, default void failedReconciliation(CustomResourceID customResourceID, RuntimeException exception) {} - default void cleanupDoneFor(CustomResourceID customResourceUid) {}; + default void cleanupDoneFor(CustomResourceID customResourceUid) {} - default void finishedReconciliation(CustomResourceID resourceID) {}; + default void finishedReconciliation(CustomResourceID resourceID) {} interface ControllerExecution { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java index 7249dfe4aa..a22d135317 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java @@ -23,11 +23,11 @@ import io.javaoperatorsdk.operator.processing.event.EventSourceManager; public class ConfiguredController> implements ResourceController, - LifecycleAware, EventSourceInitializer { + LifecycleAware, EventSourceInitializer { private final ResourceController controller; private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; - private DefaultEventSourceManager eventSourceManager; + private DefaultEventSourceManager eventSourceManager; public ConfiguredController(ResourceController controller, ControllerConfiguration configuration, @@ -38,7 +38,7 @@ public ConfiguredController(ResourceController controller, } @Override - public DeleteControl deleteResource(R resource, Context context) { + public DeleteControl deleteResource(R resource, Context context) { return configuration.getConfigurationService().getMetrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -64,7 +64,7 @@ public DeleteControl execute() { } @Override - public UpdateControl createOrUpdateResource(R resource, Context context) { + public UpdateControl createOrUpdateResource(R resource, Context context) { return configuration.getConfigurationService().getMetrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -97,7 +97,7 @@ public UpdateControl execute() { } @Override - public void prepareEventSources(EventSourceManager eventSourceManager) { + public void prepareEventSources(EventSourceManager eventSourceManager) { throw new UnsupportedOperationException("This method should never be called directly"); } @@ -170,7 +170,7 @@ public void start() throws OperatorException { try { eventSourceManager = new DefaultEventSourceManager<>(this); if (controller instanceof EventSourceInitializer) { - ((EventSourceInitializer) controller).prepareEventSources(eventSourceManager); + ((EventSourceInitializer) controller).prepareEventSources(eventSourceManager); } } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); @@ -213,7 +213,7 @@ private boolean failOnMissingCurrentNS() { return false; } - public EventSourceManager getEventSourceManager() { + public EventSourceManager getEventSourceManager() { return eventSourceManager; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java index 3000e463b6..3764667288 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java @@ -8,7 +8,13 @@ import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.BaseControl; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.DefaultContext; +import io.javaoperatorsdk.operator.api.DeleteControl; +import io.javaoperatorsdk.operator.api.ObservedGenerationAware; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; @@ -64,8 +70,8 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) return PostExecutionControl.defaultDispatch(); } - Context context = - new DefaultContext<>(executionScope.getRetryInfo()); + Context context = + new DefaultContext(executionScope.getRetryInfo()); if (markedForDeletion) { return handleDelete(resource, context); } else { @@ -92,7 +98,7 @@ private boolean shouldNotDispatchToDelete(R resource) { } private PostExecutionControl handleCreateOrUpdate( - ExecutionScope executionScope, R resource, Context context) { + ExecutionScope executionScope, R resource, Context context) { if (configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer())) { /* * We always add the finalizer if missing and the controller is configured to use a finalizer. @@ -161,7 +167,7 @@ private void updatePostExecutionControlWithReschedule( baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); } - private PostExecutionControl handleDelete(R resource, Context context) { + private PostExecutionControl handleDelete(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java index bae73dc3c3..cf9802674e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java @@ -11,7 +11,7 @@ * @param the type of custom resources handled by this filter */ @FunctionalInterface -public interface CustomResourceEventFilter { +public interface CustomResourceEventFilter> { /** * Determines whether the change between the old version of the resource and the new one needs to diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java index 7de41c455d..63584a2c86 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java @@ -8,7 +8,7 @@ */ public final class CustomResourceEventFilters { - private static final CustomResourceEventFilter USE_FINALIZER = + private static final CustomResourceEventFilter> USE_FINALIZER = (configuration, oldResource, newResource) -> { if (configuration.useFinalizer()) { final var finalizer = configuration.getFinalizer(); @@ -21,7 +21,7 @@ public final class CustomResourceEventFilters { } }; - private static final CustomResourceEventFilter GENERATION_AWARE = + private static final CustomResourceEventFilter> GENERATION_AWARE = (configuration, oldResource, newResource) -> { final var status = newResource.getStatus(); final var generationAware = configuration.isGenerationAware(); @@ -35,13 +35,13 @@ public final class CustomResourceEventFilters { oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); }; - private static final CustomResourceEventFilter PASSTHROUGH = + private static final CustomResourceEventFilter> PASSTHROUGH = (configuration, oldResource, newResource) -> true; - private static final CustomResourceEventFilter NONE = + private static final CustomResourceEventFilter> NONE = (configuration, oldResource, newResource) -> false; - private static final CustomResourceEventFilter MARKED_FOR_DELETION = + private static final CustomResourceEventFilter> MARKED_FOR_DELETION = (configuration, oldResource, newResource) -> newResource.isMarkedForDeletion(); private CustomResourceEventFilters() {} @@ -53,7 +53,7 @@ private CustomResourceEventFilters() {} * @return a filter that accepts all events */ @SuppressWarnings("unchecked") - public static CustomResourceEventFilter passthrough() { + public static > CustomResourceEventFilter passthrough() { return (CustomResourceEventFilter) PASSTHROUGH; } @@ -64,7 +64,7 @@ public static CustomResourceEventFilter passthroug * @return a filter that reject all events */ @SuppressWarnings("unchecked") - public static CustomResourceEventFilter none() { + public static > CustomResourceEventFilter none() { return (CustomResourceEventFilter) NONE; } @@ -76,7 +76,7 @@ public static CustomResourceEventFilter none() { * @return a filter accepting changes based on generation information */ @SuppressWarnings("unchecked") - public static CustomResourceEventFilter generationAware() { + public static > CustomResourceEventFilter generationAware() { return (CustomResourceEventFilter) GENERATION_AWARE; } @@ -89,7 +89,7 @@ public static CustomResourceEventFilter generation * applied */ @SuppressWarnings("unchecked") - public static CustomResourceEventFilter finalizerNeededAndApplied() { + public static > CustomResourceEventFilter finalizerNeededAndApplied() { return (CustomResourceEventFilter) USE_FINALIZER; } @@ -100,7 +100,7 @@ public static CustomResourceEventFilter finalizerN * @return a filter accepting changes based on whether the Custom Resource is marked for deletion. */ @SuppressWarnings("unchecked") - public static CustomResourceEventFilter markedForDeletion() { + public static > CustomResourceEventFilter markedForDeletion() { return (CustomResourceEventFilter) MARKED_FOR_DELETION; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java index 645c997285..eb0fddd849 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java @@ -7,9 +7,8 @@ class DeleteControlTest { @Test void cannotReScheduleForDefaultDelete() { - Assertions.assertThrows(IllegalStateException.class, () -> { - DeleteControl.defaultDelete().rescheduleAfter(1000L); - }); + Assertions.assertThrows(IllegalStateException.class, + () -> DeleteControl.defaultDelete().rescheduleAfter(1000L)); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java index 87385e61d2..1c651afa60 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java @@ -1,8 +1,6 @@ package io.javaoperatorsdk.operator.processing; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,14 +18,10 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; +import io.javaoperatorsdk.operator.processing.EventDispatcher.CustomResourceFacade; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.ADDED; -import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.UPDATED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -44,53 +38,56 @@ class EventDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; - private CustomResource testCustomResource; - private EventDispatcher eventDispatcher; - private final ResourceController controller = mock(ResourceController.class); - private final ControllerConfiguration configuration = + private TestCustomResource testCustomResource; + private EventDispatcher eventDispatcher; + private final ResourceController controller = mock(ResourceController.class); + private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); private final ConfigurationService configService = mock(ConfigurationService.class); - private final ConfiguredController> configuredController = - new ConfiguredController(controller, configuration, null); - private final EventDispatcher.CustomResourceFacade customResourceFacade = + private final CustomResourceFacade customResourceFacade = mock(EventDispatcher.CustomResourceFacade.class); @BeforeEach void setup() { - eventDispatcher = new EventDispatcher(configuredController, customResourceFacade); - testCustomResource = TestUtils.testCustomResource(); + eventDispatcher = init(testCustomResource, controller, configuration, customResourceFacade); + } + private > EventDispatcher init(R customResource, + ResourceController controller, ControllerConfiguration configuration, + CustomResourceFacade customResourceFacade) { when(configuration.getFinalizer()).thenReturn(DEFAULT_FINALIZER); when(configuration.useFinalizer()).thenCallRealMethod(); when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configService.getMetrics()).thenReturn(Metrics.NOOP); when(configuration.getConfigurationService()).thenReturn(configService); - when(controller.createOrUpdateResource(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateCustomResource(testCustomResource)); - when(controller.deleteResource(eq(testCustomResource), any())) + when(controller.createOrUpdateResource(eq(customResource), any())) + .thenReturn(UpdateControl.updateCustomResource(customResource)); + when(controller.deleteResource(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); when(customResourceFacade.replaceWithLock(any())).thenReturn(null); + ConfiguredController configuredController = + new ConfiguredController<>(controller, configuration, null); + + return new EventDispatcher<>(configuredController, customResourceFacade); } @Test void addFinalizerOnNewResource() { assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); - eventDispatcher.handleExecution( - executionScopeWithCREvent(ADDED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, never()) .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) .replaceWithLock( argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); - assertTrue(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); + assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); } @Test void callCreateOrUpdateOnNewResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - eventDispatcher.handleExecution( - executionScopeWithCREvent(ADDED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, times(1)) .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); } @@ -102,8 +99,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { when(controller.createOrUpdateResource(eq(testCustomResource), any())) .thenReturn(UpdateControl.updateStatusSubResource(testCustomResource)); - eventDispatcher.handleExecution( - executionScopeWithCREvent(ADDED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); verify(customResourceFacade, never()).replaceWithLock(any()); @@ -117,8 +113,7 @@ void updatesBothResourceAndStatusIfFinalizerSet() { .thenReturn(UpdateControl.updateCustomResourceAndStatus(testCustomResource)); when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, times(1)).replaceWithLock(testCustomResource); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); @@ -128,8 +123,7 @@ void updatesBothResourceAndStatusIfFinalizerSet() { void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, times(1)) .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); } @@ -140,20 +134,20 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { assertTrue(testCustomResource.addFinalizer(DEFAULT_FINALIZER)); markForDeletion(testCustomResource); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, times(1)).deleteResource(eq(testCustomResource), any()); } - /** Note that there could be more finalizers. Out of our control. */ + /** + * Note that there could be more finalizers. Out of our control. + */ @Test void callDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { configureToNotUseFinalizer(); markForDeletion(testCustomResource); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller).deleteResource(eq(testCustomResource), any()); } @@ -162,14 +156,14 @@ void callDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { void doNotCallDeleteIfMarkedForDeletionWhenFinalizerHasAlreadyBeenRemoved() { markForDeletion(testCustomResource); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, never()).deleteResource(eq(testCustomResource), any()); } private void configureToNotUseFinalizer() { - ControllerConfiguration configuration = mock(ControllerConfiguration.class); + ControllerConfiguration> configuration = + mock(ControllerConfiguration.class); when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configService.getMetrics()).thenReturn(Metrics.NOOP); when(configuration.getConfigurationService()).thenReturn(configService); @@ -182,8 +176,7 @@ private void configureToNotUseFinalizer() { void doesNotAddFinalizerIfConfiguredNotTo() { configureToNotUseFinalizer(); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(0, testCustomResource.getMetadata().getFinalizers().size()); } @@ -193,8 +186,7 @@ void removesDefaultFinalizerOnDeleteIfSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(0, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, times(1)).replaceWithLock(any()); @@ -208,8 +200,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { .thenReturn(DeleteControl.noFinalizerRemoval()); markForDeletion(testCustomResource); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, never()).replaceWithLock(any()); @@ -222,8 +213,7 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { when(controller.createOrUpdateResource(eq(testCustomResource), any())) .thenReturn(UpdateControl.noUpdate()); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); verify(customResourceFacade, never()).updateStatus(testCustomResource); } @@ -234,8 +224,7 @@ void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { when(controller.createOrUpdateResource(eq(testCustomResource), any())) .thenReturn(UpdateControl.noUpdate()); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, times(1)).replaceWithLock(any()); @@ -246,8 +235,7 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { removeFinalizers(testCustomResource); markForDeletion(testCustomResource); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); verify(controller, never()).deleteResource(eq(testCustomResource), any()); @@ -256,10 +244,8 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { @Test void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); - eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, times(2)).createOrUpdateResource(eq(testCustomResource), any()); } @@ -283,11 +269,11 @@ public boolean isLastAttempt() { } })); - ArgumentCaptor> contextArgumentCaptor = + ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(Context.class); verify(controller, times(1)) .createOrUpdateResource(eq(testCustomResource), contextArgumentCaptor.capture()); - Context context = contextArgumentCaptor.getValue(); + Context context = contextArgumentCaptor.getValue(); final var retryInfo = context.getRetryInfo().get(); assertThat(retryInfo.getAttemptCount()).isEqualTo(2); assertThat(retryInfo.isLastAttempt()).isEqualTo(true); @@ -301,8 +287,8 @@ void setReScheduleToPostExecutionControlFromUpdateControl() { .thenReturn( UpdateControl.updateStatusSubResource(testCustomResource).rescheduleAfter(1000L)); - PostExecutionControl control = eventDispatcher.handleExecution( - executionScopeWithCREvent(ADDED, testCustomResource)); + PostExecutionControl control = + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); } @@ -313,11 +299,10 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { markForDeletion(testCustomResource); when(controller.deleteResource(eq(testCustomResource), any())) - .thenReturn( - DeleteControl.noFinalizerRemoval().rescheduleAfter(1000L)); + .thenReturn(DeleteControl.noFinalizerRemoval().rescheduleAfter(1000L)); - PostExecutionControl control = eventDispatcher.handleExecution( - executionScopeWithCREvent(UPDATED, testCustomResource)); + PostExecutionControl control = + eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); } @@ -326,15 +311,19 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { void setObservedGenerationForStatusIfNeeded() { var observedGenResource = createObservedGenCustomResource(); - when(configuration.isGenerationAware()).thenReturn(true); - when(controller.createOrUpdateResource(eq(observedGenResource), any())) - .thenReturn( - UpdateControl.updateStatusSubResource(observedGenResource)); + ResourceController lController = mock(ResourceController.class); + ControllerConfiguration lConfiguration = + mock(ControllerConfiguration.class); + CustomResourceFacade lFacade = mock(CustomResourceFacade.class); + var lDispatcher = init(observedGenResource, lController, lConfiguration, lFacade); - when(customResourceFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource); + when(lConfiguration.isGenerationAware()).thenReturn(true); + when(lController.createOrUpdateResource(eq(observedGenResource), any())) + .thenReturn(UpdateControl.updateStatusSubResource(observedGenResource)); + when(lFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource); - PostExecutionControl control = eventDispatcher.handleExecution( - executionScopeWithCREvent(ADDED, observedGenResource)); + PostExecutionControl control = lDispatcher.handleExecution( + executionScopeWithCREvent(observedGenResource)); assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration().get()) .isEqualTo(1L); } @@ -357,13 +346,7 @@ private void removeFinalizers(CustomResource customResource) { customResource.getMetadata().getFinalizers().clear(); } - public ExecutionScope executionScopeWithCREvent( - ResourceAction action, CustomResource resource, Event... otherEvents) { - CustomResourceEvent event = - new CustomResourceEvent(action, CustomResourceID.fromResource(resource)); - List eventList = new ArrayList<>(1 + otherEvents.length); - eventList.add(event); - eventList.addAll(Arrays.asList(otherEvents)); - return new ExecutionScope(resource, null); + public > ExecutionScope executionScopeWithCREvent(T resource) { + return new ExecutionScope<>(resource, null); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index 7556d2412a..7fcda72bb4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -187,7 +187,7 @@ public MyController(Consumer consumer) { @Override public UpdateControl createOrUpdateResource( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { LOGGER.info("Received event on: {}", resource); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java index 2cef04a09a..eba0859a9c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java @@ -39,7 +39,7 @@ public TestCustomResourceController(KubernetesClient kubernetesClient, boolean u @Override public DeleteControl deleteResource( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { Boolean delete = kubernetesClient .configMaps() @@ -62,7 +62,7 @@ public DeleteControl deleteResource( @Override public UpdateControl createOrUpdateResource( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); } diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 5ffef0d184..7a9977ded0 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -34,12 +34,10 @@ org.assertj assertj-core - 3.20.2 org.awaitility awaitility - 4.1.0 diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index ba4cf40250..34ca56378f 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -12,7 +12,7 @@ import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters; -public class AnnotationConfiguration +public class AnnotationConfiguration> implements ControllerConfiguration { private final ResourceController controller; diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java index d76db8fbcd..a0fadbb8cb 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java @@ -19,8 +19,9 @@ class ClassMappingProvider { private static final Logger log = LoggerFactory.getLogger(ClassMappingProvider.class); + @SuppressWarnings("unchecked") static Map provide(final String resourcePath, T key, V value) { - Map result = new HashMap(); + Map result = new HashMap<>(); try { final var classLoader = Thread.currentThread().getContextClassLoader(); final Enumeration customResourcesMetadataList = classLoader.getResources(resourcePath); 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 099754acd1..60e65a06f4 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 @@ -19,12 +19,12 @@ public static DefaultConfigurationService instance() { } @Override - public ControllerConfiguration getConfigurationFor( + public > ControllerConfiguration getConfigurationFor( ResourceController controller) { return getConfigurationFor(controller, true); } - ControllerConfiguration getConfigurationFor( + > ControllerConfiguration getConfigurationFor( ResourceController controller, boolean createIfNeeded) { var config = super.getConfigurationFor(controller); if (config == null) { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java index 913bab6624..1536d681f3 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java @@ -5,10 +5,10 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.ResourceController; +@SuppressWarnings("rawtypes") public class RuntimeControllerMetadata { public static final String CONTROLLERS_RESOURCE_PATH = "javaoperatorsdk/controllers"; - public static final String DONEABLES_RESOURCE_PATH = "javaoperatorsdk/doneables"; private static final Map, Class> controllerToCustomResourceMappings; static { @@ -17,7 +17,7 @@ public class RuntimeControllerMetadata { CONTROLLERS_RESOURCE_PATH, ResourceController.class, CustomResource.class); } - static Class getCustomResourceClass( + static > Class getCustomResourceClass( ResourceController controller) { final Class customResourceClass = controllerToCustomResourceMappings.get(controller.getClass()); diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/TypeParameterResolver.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/TypeParameterResolver.java index 8ef145d8b9..82f46dcdf6 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/TypeParameterResolver.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/TypeParameterResolver.java @@ -77,7 +77,7 @@ private int getTypeIndexWithName( return IntStream.range(0, typeParameters.size()) .filter(i -> typeParameters.get(i).getSimpleName().toString().equals(typeName)) .findFirst() - .getAsInt(); + .orElseThrow(); } private List findChain(Types typeUtils, DeclaredType declaredType) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 56f1d8ee2e..64623470ae 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -110,13 +110,13 @@ static class TestCustomFinalizerController @Override public UpdateControl createOrUpdateResource( - InnerCustomResource resource, Context context) { + InnerCustomResource resource, Context context) { return null; } @Group("test.crd") @Version("v1") - public class InnerCustomResource extends CustomResource { + public static class InnerCustomResource extends CustomResource { } } @@ -127,7 +127,7 @@ static class NotAutomaticallyCreated implements ResourceController createOrUpdateResource( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { return null; } } @@ -137,7 +137,7 @@ static class TestCustomResourceController implements ResourceController createOrUpdateResource( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { return null; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/TestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/TestCustomResource.java index 190b73690f..0f94ae92e4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/TestCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/TestCustomResource.java @@ -7,5 +7,5 @@ @Group("sample.javaoperatorsdk") @Version("v1") -class TestCustomResource extends CustomResource implements Namespaced { +class TestCustomResource extends CustomResource implements Namespaced { } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java index 74c313e87d..df9f3dcf10 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java @@ -6,7 +6,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @Controller @@ -21,7 +24,7 @@ public class DoubleUpdateTestCustomResourceController @Override public UpdateControl createOrUpdateResource( - DoubleUpdateTestCustomResource resource, Context context) { + DoubleUpdateTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); log.info("Value: " + resource.getSpec().getValue()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java index caa5ad9fda..cecf713f3c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java @@ -7,7 +7,11 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.api.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @@ -35,7 +39,7 @@ public void prepareEventSources(EventSourceManager eventSourceManager) { @Override public UpdateControl createOrUpdateResource( - EventSourceTestCustomResource resource, Context context) { + EventSourceTestCustomResource resource, Context context) { timerEventSource.schedule(resource, TIMER_DELAY, TIMER_PERIOD); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java index 772c93cac3..ad2a2dc863 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java @@ -5,7 +5,11 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.api.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; @@ -41,7 +45,7 @@ public void prepareEventSources(EventSourceManager eventSourceManager) { @Override public UpdateControl createOrUpdateResource( InformerEventSourceTestCustomResource resource, - Context context) { + Context context) { // Reading the config map from the informer not from the API // name of the config map same as custom resource for sake of simplicity diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java index 9c5e363bc5..cf24ee9484 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java @@ -28,7 +28,7 @@ public class RetryTestCustomResourceController @Override public UpdateControl createOrUpdateResource( - RetryTestCustomResource resource, Context context) { + RetryTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java index b6aabd1e67..6aefaa3922 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java @@ -63,7 +63,7 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { @Override public DeleteControl deleteResource( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { Boolean delete = kubernetesClient .configMaps() @@ -86,7 +86,7 @@ public DeleteControl deleteResource( @Override public UpdateControl createOrUpdateResource( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java index 82157cf229..f04958cbcd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java @@ -26,7 +26,7 @@ public class SubResourceTestCustomResourceController @Override public UpdateControl createOrUpdateResource( - SubResourceTestCustomResource resource, Context context) { + SubResourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); diff --git a/operator-framework/src/test/resources/compile-fixtures/AbstractController.java b/operator-framework/src/test/resources/compile-fixtures/AbstractController.java index 0acdf6be15..d35a18c070 100644 --- a/operator-framework/src/test/resources/compile-fixtures/AbstractController.java +++ b/operator-framework/src/test/resources/compile-fixtures/AbstractController.java @@ -5,10 +5,10 @@ import java.io.Serializable; -public abstract class AbstractController implements Serializable, +public abstract class AbstractController> implements Serializable, ResourceController { - public static class MyCustomResource extends CustomResource { + public static class MyCustomResource extends CustomResource { } } diff --git a/operator-framework/src/test/resources/compile-fixtures/AdditionalControllerInterface.java b/operator-framework/src/test/resources/compile-fixtures/AdditionalControllerInterface.java index e5d95ab57a..01077510f5 100644 --- a/operator-framework/src/test/resources/compile-fixtures/AdditionalControllerInterface.java +++ b/operator-framework/src/test/resources/compile-fixtures/AdditionalControllerInterface.java @@ -5,7 +5,7 @@ import java.io.Serializable; -public interface AdditionalControllerInterface extends +public interface AdditionalControllerInterface> extends Serializable, ResourceController { } diff --git a/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java b/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java index e56943fa30..2b0fa58a8f 100644 --- a/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java +++ b/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java @@ -11,16 +11,16 @@ @Controller public class ControllerImplemented2Interfaces implements Serializable, ResourceController { - public static class MyCustomResource extends CustomResource { + public static class MyCustomResource extends CustomResource { } @Override - public UpdateControl createOrUpdateResource(MyCustomResource customResource, Context context) { + public UpdateControl createOrUpdateResource(MyCustomResource customResource, Context context) { return UpdateControl.updateCustomResource(null); } @Override - public DeleteControl deleteResource(MyCustomResource customResource, Context context) { + public DeleteControl deleteResource(MyCustomResource customResource, Context context) { return DeleteControl.defaultDelete(); } } diff --git a/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java b/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java index 4f5619b4a4..5674a3ed81 100644 --- a/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java +++ b/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java @@ -12,12 +12,12 @@ public class ControllerImplementedIntermediateAbstractClass extends public UpdateControl createOrUpdateResource( AbstractController.MyCustomResource customResource, - Context context) { + Context context) { return UpdateControl.updateCustomResource(null); } public DeleteControl deleteResource(AbstractController.MyCustomResource customResource, - Context context) { + Context context) { return DeleteControl.defaultDelete(); } } diff --git a/operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractController.java b/operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractController.java index 604652e879..4fe30adfe3 100644 --- a/operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractController.java +++ b/operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractController.java @@ -4,7 +4,7 @@ import java.io.Serializable; -public abstract class MultilevelAbstractController implements +public abstract class MultilevelAbstractController> implements Serializable, AdditionalControllerInterface { diff --git a/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java b/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java index d941a16d9d..0c07065337 100644 --- a/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java +++ b/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java @@ -10,18 +10,18 @@ public class MultilevelController extends MultilevelAbstractController { - public static class MyCustomResource extends CustomResource { + public static class MyCustomResource extends CustomResource { } public UpdateControl createOrUpdateResource( MultilevelController.MyCustomResource customResource, - Context context) { + Context context) { return UpdateControl.updateCustomResource(null); } public DeleteControl deleteResource(MultilevelController.MyCustomResource customResource, - Context context) { + Context context) { return DeleteControl.defaultDelete(); } diff --git a/pom.xml b/pom.xml index def68a848c..3efc3b0f38 100644 --- a/pom.xml +++ b/pom.xml @@ -301,6 +301,7 @@ format + ${josdk.project.root}/contributing/eclipse-google-style.xml diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java index 67fb81dbd7..932fd1bc1b 100644 --- a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java +++ b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java @@ -34,14 +34,14 @@ public CustomServiceController(KubernetesClient kubernetesClient) { } @Override - public DeleteControl deleteResource(CustomService resource, Context context) { + public DeleteControl deleteResource(CustomService resource, Context context) { log.info("Execution deleteResource for: {}", resource.getMetadata().getName()); return DeleteControl.defaultDelete(); } @Override public UpdateControl createOrUpdateResource( - CustomService resource, Context context) { + CustomService resource, Context context) { log.info("Execution createOrUpdateResource for: {}", resource.getMetadata().getName()); ServicePort servicePort = new ServicePort(); From b0a16dd3f3d0c028c33ef63288f87238ed1dc712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 29 Oct 2021 10:14:56 +0200 Subject: [PATCH 0112/1608] fix: minor improvements on tests (#640) --- .../operator/processing/DefaultEventHandlerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java index 2ffabd6fe1..9be173e41b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java @@ -101,7 +101,7 @@ public void schedulesAnEventRetryOnException() { } @Test - public void executesTheControllerInstantlyAfterErrorIfEventsBuffered() { + public void executesTheControllerInstantlyAfterErrorIfNewEventsReceived() { Event event = prepareCREvent(); TestCustomResource customResource = testCustomResource(); overrideData(event.getRelatedCustomResourceID(), customResource); @@ -114,7 +114,7 @@ public void executesTheControllerInstantlyAfterErrorIfEventsBuffered() { // start processing an event defaultEventHandlerWithRetry.handleEvent(event); - // buffer another event + // handle another event defaultEventHandlerWithRetry.handleEvent(event); ArgumentCaptor executionScopeArgumentCaptor = @@ -180,7 +180,7 @@ public void scheduleTimedEventIfInstructedByPostExecutionControl() { } @Test - public void reScheduleOnlyIfNotExecutedBufferedEvents() { + public void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { var testDelay = 10000L; when(eventDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); From 1bb920ccd203a514e3699314add9d278147529a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 29 Oct 2021 10:12:39 +0200 Subject: [PATCH 0113/1608] docs: webpage docs skeleton (#631) --- README.md | 1 + docs/Gemfile.lock | 4 ++- docs/_data/sidebar.yml | 11 ++++++-- docs/documentation/faq.md | 7 +++++ docs/documentation/features.md | 39 +++++++++++++++++++++++++++ docs/documentation/intro-operators.md | 12 +++++++++ 6 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 docs/documentation/faq.md create mode 100644 docs/documentation/features.md create mode 100644 docs/documentation/intro-operators.md diff --git a/README.md b/README.md index 2988e55408..d42c45b7a0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Build Kubernetes Operators in Java without hassle. Inspired by [operator-sdk](https://github.com/operator-framework/operator-sdk). +Our webpage with documentation is getting better every day: https://javaoperatorsdk.io/ Table of Contents ========== diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index e8ee3a5427..605cd1d411 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -74,11 +74,13 @@ GEM unicode-display_width (1.7.0) PLATFORMS + ruby universal-darwin-20 + x86_64-linux DEPENDENCIES jekyll (~> 4.2) jekyll-github-metadata BUNDLED WITH - 2.2.22 + 2.2.30 diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 96266dd842..7d88cde491 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -1,7 +1,14 @@ # Navbar menu navigation links + - title: Intro to Operators + url: /docs/intro-operators - title: Getting Started url: /docs/getting-started - - title: Contributing - url: /docs/contributing - title: How to use Samples url: /docs/using-samples + - title: Features + url: /docs/features + - title: FAQ + url: /docs/faq + - title: Contributing + url: /docs/contributing + diff --git a/docs/documentation/faq.md b/docs/documentation/faq.md new file mode 100644 index 0000000000..f4127b39fe --- /dev/null +++ b/docs/documentation/faq.md @@ -0,0 +1,7 @@ +--- +title: FAQ +description: Frequently asked questions +layout: docs +permalink: /docs/faq +--- + diff --git a/docs/documentation/features.md b/docs/documentation/features.md new file mode 100644 index 0000000000..7f77bcbbde --- /dev/null +++ b/docs/documentation/features.md @@ -0,0 +1,39 @@ +--- +title: Features +description: Features of the SDK +layout: docs +permalink: /docs/features +--- + +# Features + +## Controller Registration + +## Configurations + +## Finalizers + +### When not to Use Finalizers + +## Automatic Retries on Error + +### Correctness and automatic retry + +## Re-Scheduling Execution + +## Retry and Re-Scheduling Common Behavior + +## Handling Related Events with Event Sources + +### Caching and Event Sources + +### The CustomResourceEventSource + +### Built-in Event Sources + +## Monitoring with Micrometer + + + + + diff --git a/docs/documentation/intro-operators.md b/docs/documentation/intro-operators.md new file mode 100644 index 0000000000..13a9812df1 --- /dev/null +++ b/docs/documentation/intro-operators.md @@ -0,0 +1,12 @@ +--- +title: Introduction to Operators +description: Introduction to Operators +layout: docs +permalink: /docs/intro-operators +--- + +# Introduction To Operators + +On this page we selected a collection of resources to introduce you to the concepts of Kubernetes Operators. + + From 1b11546d77e7be14d8ffce5f0efcb05f10a660f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 29 Oct 2021 11:12:43 +0200 Subject: [PATCH 0114/1608] feature!: throw exception if controller cannot be registered (#641) * feature!: throw exception if controller cannot be registered If a controller cannot be registered because of cofirguration loading is a programatic error, should be reported asap. Logging just a warning would make it hard to find out where is the problem and why the controller not starting --- .../java/io/javaoperatorsdk/operator/Operator.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 ae094b25c6..8812d8d6b3 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 @@ -137,12 +137,11 @@ public void close() { throws OperatorException { final var existing = configurationService.getConfigurationFor(controller); if (existing == null) { - log.warn( - "Skipping registration of {} controller named {} because its configuration cannot be found.\n" - + "Known controllers are: {}", - controller.getClass().getCanonicalName(), - ControllerUtils.getNameFor(controller), - configurationService.getKnownControllerNames()); + throw new OperatorException( + "Cannot register controller with name " + controller.getClass().getCanonicalName() + + " controller named " + ControllerUtils.getNameFor(controller) + + " because its configuration cannot be found.\n" + + " Known controllers are: " + configurationService.getKnownControllerNames()); } else { if (configuration == null) { configuration = existing; From cc454a3a76a4b00e499f3d2d63fe45d3eb8e61b1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 28 Oct 2021 20:30:49 +0200 Subject: [PATCH 0115/1608] fix: replace jandex plugin by Quarkus detection of beans.xml files This allows to not run into compatibility between index versions. --- .../src/main/resources/META-INF/beans.xml | 0 pom.xml | 18 ------------------ 2 files changed, 18 deletions(-) create mode 100644 operator-framework-core/src/main/resources/META-INF/beans.xml diff --git a/operator-framework-core/src/main/resources/META-INF/beans.xml b/operator-framework-core/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pom.xml b/pom.xml index 3efc3b0f38..67b8b4efe4 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,6 @@ 2.8.2 2.5.2 5.0.0 - 1.2.1 2.16.0 1.0 1.6.2 @@ -223,11 +222,6 @@ maven-install-plugin ${maven-install-plugin.version} - - org.jboss.jandex - jandex-maven-plugin - ${jandex-maven-plugin.version} - net.revelc.code.formatter formatter-maven-plugin @@ -268,18 +262,6 @@ - - org.jboss.jandex - jandex-maven-plugin - - - make-index - - jandex - - - - org.apache.maven.plugins maven-surefire-plugin From 22daa8a2998b7df4f69a3b8486e5923d0d641c5c Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 28 Oct 2021 21:02:03 +0200 Subject: [PATCH 0116/1608] fix: improve duplicated controller detection, add tests While we originally planned to make it possible to register controllers with the same CR but with different version (see #637), that behavior should actually be forbidden since only one CR version can be served, see #94 for more details. --- .../io/javaoperatorsdk/operator/Operator.java | 9 +-- .../operator/ControllerManagerTest.java | 76 +++++++++++++++++++ .../sample/simple/DuplicateCRController.java | 16 ++++ .../TestCustomResourceControllerV2.java | 16 ++++ .../sample/simple/TestCustomResourceV2.java | 12 +++ 5 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceControllerV2.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java 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 8812d8d6b3..60fe670a36 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 @@ -162,11 +162,10 @@ public void close() { } } - private static class ControllerManager implements LifecycleAware { + static class ControllerManager implements LifecycleAware { private final Map controllers = new HashMap<>(); private boolean started = false; - public synchronized void shouldStart() { if (started) { return; @@ -199,9 +198,9 @@ public synchronized void add(ConfiguredController configuredController) { final var crdName = configuration.getCRDName(); final var existing = controllers.get(crdName); if (existing != null) { - throw new OperatorException("Cannot register controller " + configuration.getName() - + ": another controller (" + existing.getConfiguration().getName() - + ") is already registered for CRD " + crdName); + throw new OperatorException("Cannot register controller '" + configuration.getName() + + "': another controller named '" + existing.getConfiguration().getName() + + "' is already registered for CRD '" + crdName + "'"); } this.controllers.put(crdName, configuredController); if (started) { 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 new file mode 100644 index 0000000000..d25378499e --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerManagerTest.java @@ -0,0 +1,76 @@ +package io.javaoperatorsdk.operator; + +import org.junit.Test; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.Operator.ControllerManager; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; +import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.sample.simple.DuplicateCRController; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceControllerV2; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceV2; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ControllerManagerTest { + + @Test + public void shouldNotAddMultipleControllersForSameCustomResource() { + final var registered = new TestControllerConfiguration<>(new TestCustomResourceController(null), + TestCustomResource.class); + final var duplicated = + new TestControllerConfiguration<>(new DuplicateCRController(), TestCustomResource.class); + + checkException(registered, duplicated); + } + + @Test + public void addingMultipleControllersForCustomResourcesWithDifferentVersionsShouldNotWork() { + final var registered = new TestControllerConfiguration<>(new TestCustomResourceController(null), + TestCustomResource.class); + final var duplicated = new TestControllerConfiguration<>(new TestCustomResourceControllerV2(), + TestCustomResourceV2.class); + + checkException(registered, duplicated); + + } + + private , U extends CustomResource> void checkException( + TestControllerConfiguration registered, + TestControllerConfiguration duplicated) { + final var exception = assertThrows(OperatorException.class, () -> { + final var controllerManager = new ControllerManager(); + controllerManager.add(new ConfiguredController<>(registered.controller, registered, null)); + controllerManager.add(new ConfiguredController<>(duplicated.controller, duplicated, null)); + }); + final var msg = exception.getMessage(); + assertTrue( + msg.contains("Cannot register controller '" + duplicated.getControllerName() + "'") + && msg.contains(registered.getControllerName()) + && msg.contains(registered.getCRDName())); + } + + private static class TestControllerConfiguration> + extends DefaultControllerConfiguration { + private final ResourceController controller; + + public TestControllerConfiguration(ResourceController controller, Class crClass) { + super(null, getControllerName(controller), + CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null); + this.controller = controller; + } + + static > String getControllerName( + ResourceController controller) { + return controller.getClass().getSimpleName() + "Controller"; + } + + private String getControllerName() { + return getControllerName(controller); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java new file mode 100644 index 0000000000..443c4c21cc --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.simple; + +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; + +@Controller +public class DuplicateCRController implements ResourceController { + + @Override + public UpdateControl createOrUpdateResource(TestCustomResource resource, + Context context) { + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceControllerV2.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceControllerV2.java new file mode 100644 index 0000000000..29a89381e2 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceControllerV2.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.simple; + +import io.javaoperatorsdk.operator.api.Context; +import io.javaoperatorsdk.operator.api.Controller; +import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.UpdateControl; + +@Controller +public class TestCustomResourceControllerV2 implements ResourceController { + + @Override + public UpdateControl createOrUpdateResource(TestCustomResourceV2 resource, + Context context) { + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java new file mode 100644 index 0000000000..e02e359bcc --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.sample.simple; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk.io") +@Version("v2") +public class TestCustomResourceV2 + extends CustomResource { + +} From 809cc56ec051810071e9753be27e3c5ccf4ae7f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 08:04:31 +0100 Subject: [PATCH 0117/1608] chore(deps): bump formatter-maven-plugin from 2.16.0 to 2.17.0 (#644) Bumps [formatter-maven-plugin](https://github.com/revelc/formatter-maven-plugin) from 2.16.0 to 2.17.0. - [Release notes](https://github.com/revelc/formatter-maven-plugin/releases) - [Changelog](https://github.com/revelc/formatter-maven-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/revelc/formatter-maven-plugin/compare/formatter-maven-plugin-2.16.0...formatter-maven-plugin-2.17.0) --- updated-dependencies: - dependency-name: net.revelc.code.formatter:formatter-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 67b8b4efe4..0ecc9ba61a 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 2.8.2 2.5.2 5.0.0 - 2.16.0 + 2.17.0 1.0 1.6.2 From 530a7ee282c5f0320b7c7a33ec34df20905291a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 1 Nov 2021 08:54:52 +0100 Subject: [PATCH 0118/1608] docs: link for basic operato articles (#643) --- docs/documentation/intro-operators.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/documentation/intro-operators.md b/docs/documentation/intro-operators.md index 13a9812df1..f69cb2895a 100644 --- a/docs/documentation/intro-operators.md +++ b/docs/documentation/intro-operators.md @@ -7,6 +7,12 @@ permalink: /docs/intro-operators # Introduction To Operators -On this page we selected a collection of resources to introduce you to the concepts of Kubernetes Operators. +This page provides a selection of articles that gives an introduction to Kubernetes operators. +## Operators in General + - [Introduction of the concept of Kubernetes Operators](https://blog.container-solutions.com/kubernetes-operators-explained) + - [Operator pattern explained in Kubernetes documentation](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) + - [An explanation why Java Operators makes sense](https://blog.container-solutions.com/cloud-native-java-infrastructure-automation-with-kubernetes-operators) + - [What are the problems an operator framework is solving](https://csviri.medium.com/deep-dive-building-a-kubernetes-operator-sdk-for-java-developers-5008218822cb) + From 5d23dca512d9372764d1add63e9d5a66637fc250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 2 Nov 2021 11:07:27 +0100 Subject: [PATCH 0119/1608] feat: contextual information about processing for loging using MDC (#642) --- docs/documentation/features.md | 17 ++++++ operator-framework-core/pom.xml | 12 ++++ .../processing/DefaultEventHandler.java | 61 +++++++++++-------- .../operator/processing/MDCUtils.java | 47 ++++++++++++++ .../internal/CustomResourceEventSource.java | 25 +++++--- .../javaoperatorsdk/operator/TestUtils.java | 1 - .../src/test/resources/log4j2.xml | 2 +- 7 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 7f77bcbbde..9b8e2fb099 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -33,6 +33,23 @@ permalink: /docs/features ## Monitoring with Micrometer +## Contextual Info for Logging with MDC + +Logging is enhanced with additional contextual information using [MDC](http://www.slf4j.org/manual.html#mdc). +This following attributes are available in most parts of reconciliation logic and during the execution of the controller: + +| MDC Key | Value added from Custom Resource | +| :--- | :--- | +| `resource.apiVersion` | `.apiVersion` | +| `resource.kind` | `.kind` | +| `resource.name` | `.metadata.name` | +| `resource.namespace` | `.metadata.namespace` | +| `resource.resourceVersion` | `.metadata.resourceVersion` | +| `resource.generation` | `.metadata.generation` | +| `resource.uid` | `.metadata.uid` | + +For more information about MDC see this [link](https://www.baeldung.com/mdc-in-log4j-2-logback). + diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index ce8115829a..af0737bf29 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -62,6 +62,18 @@ org.slf4j slf4j-api + + org.apache.logging.log4j + log4j-slf4j-impl + test + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + test-jar + test + org.junit.jupiter diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java index 212cd378d7..9b5c10d877 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java @@ -103,6 +103,7 @@ public void handleEvent(Event event) { return; } final var resourceID = event.getRelatedCustomResourceID(); + MDCUtils.addCustomResourceIDInfo(resourceID); metrics.receivedEvent(event); handleEventMarking(event); @@ -111,40 +112,44 @@ public void handleEvent(Event event) { } else { cleanupForDeletedEvent(resourceID); } - } finally { lock.unlock(); + MDCUtils.removeCustomResourceIDInfo(); } } private void submitReconciliationExecution(CustomResourceID customResourceUid) { - boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); - Optional latestCustomResource = - resourceCache.getCustomResource(customResourceUid); - - if (!controllerUnderExecution - && latestCustomResource.isPresent()) { - setUnderExecutionProcessing(customResourceUid); - final var retryInfo = retryInfo(customResourceUid); - ExecutionScope executionScope = - new ExecutionScope<>( - latestCustomResource.get(), - retryInfo); - eventMarker.unMarkEventReceived(customResourceUid); - metrics.reconcileCustomResource(customResourceUid, retryInfo); - log.debug("Executing events for custom resource. Scope: {}", executionScope); - executor.execute(new ControllerExecution(executionScope)); - } else { - log.debug( - "Skipping executing controller for resource id: {}." - + " Controller in execution: {}. Latest CustomResource present: {}", - customResourceUid, - controllerUnderExecution, - latestCustomResource.isPresent()); - if (latestCustomResource.isEmpty()) { - log.warn("no custom resource found in cache for CustomResourceID: {}", - customResourceUid); + try { + boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); + Optional latestCustomResource = + resourceCache.getCustomResource(customResourceUid); + latestCustomResource.ifPresent(MDCUtils::addCustomResourceInfo); + if (!controllerUnderExecution + && latestCustomResource.isPresent()) { + setUnderExecutionProcessing(customResourceUid); + final var retryInfo = retryInfo(customResourceUid); + ExecutionScope executionScope = + new ExecutionScope<>( + latestCustomResource.get(), + retryInfo); + eventMarker.unMarkEventReceived(customResourceUid); + metrics.reconcileCustomResource(customResourceUid, retryInfo); + log.debug("Executing events for custom resource. Scope: {}", executionScope); + executor.execute(new ControllerExecution(executionScope)); + } else { + log.debug( + "Skipping executing controller for resource id: {}." + + " Controller in execution: {}. Latest CustomResource present: {}", + customResourceUid, + controllerUnderExecution, + latestCustomResource.isPresent()); + if (latestCustomResource.isEmpty()) { + log.warn("no custom resource found in cache for CustomResourceID: {}", + customResourceUid); + } } + } finally { + MDCUtils.removeCustomResourceInfo(); } } @@ -351,6 +356,7 @@ public void run() { final var thread = Thread.currentThread(); final var name = thread.getName(); try { + MDCUtils.addCustomResourceInfo(executionScope.getCustomResource()); thread.setName("EventHandler-" + controllerName); PostExecutionControl postExecutionControl = eventDispatcher.handleExecution(executionScope); @@ -358,6 +364,7 @@ public void run() { } finally { // restore original name thread.setName(name); + MDCUtils.removeCustomResourceInfo(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java new file mode 100644 index 0000000000..e68312c451 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java @@ -0,0 +1,47 @@ +package io.javaoperatorsdk.operator.processing; + +import org.slf4j.MDC; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; + +public class MDCUtils { + + private static final String NAME = "resource.name"; + private static final String NAMESPACE = "resource.namespace"; + private static final String API_VERSION = "resource.apiVersion"; + private static final String KIND = "resource.kind"; + private static final String RESOURCE_VERSION = "resource.resourceVersion"; + private static final String GENERATION = "resource.generation"; + private static final String UID = "resource.uid"; + + public static void addCustomResourceIDInfo(CustomResourceID customResourceID) { + MDC.put(NAME, customResourceID.getName()); + MDC.put(NAMESPACE, customResourceID.getNamespace().orElse("no namespace")); + } + + public static void removeCustomResourceIDInfo() { + MDC.remove(NAME); + MDC.remove(NAMESPACE); + } + + public static void addCustomResourceInfo(CustomResource customResource) { + MDC.put(API_VERSION, customResource.getApiVersion()); + MDC.put(KIND, customResource.getKind()); + MDC.put(NAME, customResource.getMetadata().getName()); + MDC.put(NAMESPACE, customResource.getMetadata().getNamespace()); + MDC.put(RESOURCE_VERSION, customResource.getMetadata().getResourceVersion()); + MDC.put(GENERATION, customResource.getMetadata().getGeneration().toString()); + MDC.put(UID, customResource.getMetadata().getUid()); + } + + public static void removeCustomResourceInfo() { + MDC.remove(API_VERSION); + MDC.remove(KIND); + MDC.remove(NAME); + MDC.remove(NAMESPACE); + MDC.remove(RESOURCE_VERSION); + MDC.remove(GENERATION); + MDC.remove(UID); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index a9a80c10f3..8845c599c6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -15,6 +15,7 @@ import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.ResourceCache; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; @@ -116,17 +117,21 @@ public void stop() { } public void eventReceived(ResourceAction action, T customResource, T oldResource) { - log.debug( - "Event received for resource: {}", getName(customResource)); - - if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { - eventHandler.handleEvent( - new CustomResourceEvent(action, CustomResourceID.fromResource(customResource))); - } else { + try { log.debug( - "Skipping event handling resource {} with version: {}", - getUID(customResource), - getVersion(customResource)); + "Event received for resource: {}", getName(customResource)); + MDCUtils.addCustomResourceInfo(customResource); + if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { + eventHandler.handleEvent( + new CustomResourceEvent(action, CustomResourceID.fromResource(customResource))); + } else { + log.debug( + "Skipping event handling resource {} with version: {}", + getUID(customResource), + getVersion(customResource)); + } + } finally { + MDCUtils.removeCustomResourceInfo(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index 49dc35abdb..5e15fd59c8 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -37,7 +37,6 @@ public static TestCustomResource testCustomResource(CustomResourceID id) { .withNamespace(id.getNamespace().orElse(null)) .build()); resource.getMetadata().setAnnotations(new HashMap<>()); - resource.setKind("CustomService"); resource.setSpec(new TestCustomResourceSpec()); resource.getSpec().setConfigMapName("test-config-map"); resource.getSpec().setKey("test-key"); diff --git a/operator-framework-core/src/test/resources/log4j2.xml b/operator-framework-core/src/test/resources/log4j2.xml index f23cf772dd..68add1ab41 100644 --- a/operator-framework-core/src/test/resources/log4j2.xml +++ b/operator-framework-core/src/test/resources/log4j2.xml @@ -2,7 +2,7 @@ - + From 52b231af7e6b1af47b20d51f78d0d69a7de7831a Mon Sep 17 00:00:00 2001 From: csviri Date: Wed, 3 Nov 2021 11:29:38 +0100 Subject: [PATCH 0120/1608] docs: finalizers and best braticies page --- docs/_data/sidebar.yml | 2 + docs/documentation/features.md | 64 +++++++++++++++++-- docs/documentation/patterns-best-practices.md | 28 ++++++++ 3 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 docs/documentation/patterns-best-practices.md diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 7d88cde491..a9a43907a2 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -7,6 +7,8 @@ url: /docs/using-samples - title: Features url: /docs/features + - title: Patterns and Best Practices + url: /docs/patterns-best-practices - title: FAQ url: /docs/faq - title: Contributing diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 9b8e2fb099..f87fd45e5e 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -5,15 +5,69 @@ layout: docs permalink: /docs/features --- -# Features +# Features -## Controller Registration +Java Operator SDK is a high level framework and related tooling in order to facilitate implementation of Kubernetes +operators. The features are by default following the best practices in an opinionated way. However, feature flags and +other configuration options are provided to fine tune or turn off these features. -## Configurations +## Controller Execution in a Nutshell -## Finalizers +Controller execution is always triggered by an event. Events typically come from the custom resource +(i.e. custom resource is created, updated or deleted) that the controller is watching, but also from different sources +(see event sources). When an event is received reconciliation is executed, unless there is already a reconciliation +happening for a particular custom resource. In other words it is guaranteed by the framework that no concurrent +reconciliation happens for a custom resource. -### When not to Use Finalizers +After a reconciliation ( +i.e. [ResourceController](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java) +called), a post-processing phase follows, where typically framework checks if: + +- an exception was thrown during execution, if yes schedules a retry. +- there are new events received during the controller execution, if yes schedule the execution again. +- there is an instruction to re-schedule the execution for the future, if yes schedule a timer event with the specified + delay. +- if none above, the reconciliation is finished. + +Briefly, in the hearth of the execution is an eventing system, where events are the triggers of the reconciliation +execution. + +## Finalizer Support + +[Kubernetes finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) +make sure that a reconciliation happens when a custom resource is instructed to be deleted. Typical case when it's +useful, when an operator is down (pod not running). Without a finalizer the reconciliation - thus the cleanup +i.e. [`ResourceController.deleteResource(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java) + +- would not happen if a custom resource is deleted. + +Finalizers are automatically added by the framework as the first step, thus when a custom resource is created, but +before the first reconciliation, the custom resource is updated via a Kubernetes API call. As a result of this update, the +finalizer will be present. The subsequent event will be received, which will trigger the first reconciliation. + +The finalizer that is automatically added will be also removed after the `deleteResource` is executed on the controller. +However, the removal behavior can be further customized, and can be instructed to "not remove yet" - this is useful just +in some specific corner cases, when there would be a long waiting period for some dependent resource cleanup. + +The name of the finalizers can be specified, in case it is not, a name will be generated. + +This behavior can be turned off, so when configured no finalizer will be added or removed. +See [`@Controller`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java) +annotation for more details. + +### When not to Use Finalizers? + +Typically, automated finalizer handling should be turned off, when **all** the cleanup of the dependent resources is +handled by Kubernetes itself. This is handled by +Kubernetes [garbage collection](https://kubernetes.io/docs/concepts/architecture/garbage-collection/#owners-dependents). +Setting the owner reference and related fields are not in the scope of the SDK for now, it's up to the user to have them +configured properly when creating the objects. + +When automatic finalizer handling is turned off, the `ResourceController.deleteResource(...)` method is not called, in +case of a delete event received. So it does not make sense to implement this method and turn off finalizer at the same +time. + +## Separating `createOrUpdate` from `delete` ## Automatic Retries on Error diff --git a/docs/documentation/patterns-best-practices.md b/docs/documentation/patterns-best-practices.md new file mode 100644 index 0000000000..c5103de3ff --- /dev/null +++ b/docs/documentation/patterns-best-practices.md @@ -0,0 +1,28 @@ +--- +title: Patterns and Best Practices +description: Patterns and Best Practices Implementing a Controller +layout: docs +permalink: /docs/patterns-best-practices +--- + +# Patterns and Best Practices + +This document describes patters and best practices, to build and run operators, and how to implement them in terms +of Java Operator SDK. + +## Implementing a Controller + +### Sync of Async Way of Resource Handling + +### Idempotency + +## Why to Have Automated Retries? + +## Managing State + +## Dependent Resources + +### EventSources and Caching + +### Why are Events Irrelevant? + From a4eccb12d6d192e97954638b1ab9c0b709b43c6b Mon Sep 17 00:00:00 2001 From: csviri Date: Wed, 3 Nov 2021 13:37:29 +0100 Subject: [PATCH 0121/1608] fix: typo --- docs/documentation/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/features.md b/docs/documentation/features.md index f87fd45e5e..9913529c58 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -21,7 +21,7 @@ reconciliation happens for a custom resource. After a reconciliation ( i.e. [ResourceController](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java) -called), a post-processing phase follows, where typically framework checks if: +called, a post-processing phase follows, where typically framework checks if: - an exception was thrown during execution, if yes schedules a retry. - there are new events received during the controller execution, if yes schedule the execution again. From f59844bbee2b2fedff87b7442166b116b42d692e Mon Sep 17 00:00:00 2001 From: csviri Date: Wed, 3 Nov 2021 13:47:37 +0100 Subject: [PATCH 0122/1608] fix: jekyll build warning with highliting --- docs/_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index 0e349390ae..a2b8fba679 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -13,7 +13,7 @@ images: /assets/images/ # Rouge highlighter markdown: kramdown -highlighter: none +highlighter: rouge kramdown: parse_block_html: true From c335dc842f2f43aa6f51a907c75090fc63692a5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Nov 2021 08:57:43 +0100 Subject: [PATCH 0123/1608] chore(deps): bump auto-service from 1.0 to 1.0.1 (#652) Bumps [auto-service](https://github.com/google/auto) from 1.0 to 1.0.1. - [Release notes](https://github.com/google/auto/releases) - [Commits](https://github.com/google/auto/compare/auto-value-1.0...auto-common-1.0.1) --- updated-dependencies: - dependency-name: com.google.auto.service:auto-service dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0ecc9ba61a..9f29e7f03b 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 2.14.1 4.0.0 3.12.0 - 1.0 + 1.0.1 0.19 1.13.0 3.21.0 From 450a1ccddc5018a5c9d80cb0f37ada0ce554bd38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 4 Nov 2021 14:45:24 +0100 Subject: [PATCH 0124/1608] fix!: remove periodic schedule from timer event source (#653) --- .../event/internal/TimerEventSource.java | 23 -------------- .../event/internal/TimerEventSourceTest.java | 30 ------------------- ...entSourceTestCustomResourceController.java | 10 +------ 3 files changed, 1 insertion(+), 62 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index 4fc78ebb44..7fda09df1f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -20,21 +20,7 @@ public class TimerEventSource> extends AbstractEv private final Timer timer = new Timer(); private final AtomicBoolean running = new AtomicBoolean(); private final Map onceTasks = new ConcurrentHashMap<>(); - private final Map timerTasks = new ConcurrentHashMap<>(); - public void schedule(R customResource, long delay, long period) { - if (!running.get()) { - throw new IllegalStateException("The TimerEventSource is not running"); - } - - CustomResourceID resourceUid = CustomResourceID.fromResource(customResource); - if (timerTasks.containsKey(resourceUid)) { - return; - } - EventProducerTimeTask task = new EventProducerTimeTask(resourceUid); - timerTasks.put(resourceUid, task); - timer.schedule(task, delay, period); - } public void scheduleOnce(R customResource, long delay) { if (!running.get()) { @@ -51,17 +37,9 @@ public void scheduleOnce(R customResource, long delay) { @Override public void cleanupForCustomResource(CustomResourceID customResourceUid) { - cancelSchedule(customResourceUid); cancelOnceSchedule(customResourceUid); } - public void cancelSchedule(CustomResourceID customResourceID) { - TimerTask timerTask = timerTasks.remove(customResourceID); - if (timerTask != null) { - timerTask.cancel(); - } - } - public void cancelOnceSchedule(CustomResourceID customResourceUid) { TimerTask timerTask = onceTasks.remove(customResourceUid); if (timerTask != null) { @@ -78,7 +56,6 @@ public void start() { public void stop() { running.set(false); onceTasks.keySet().forEach(this::cancelOnceSchedule); - timerTasks.keySet().forEach(this::cancelSchedule); timer.cancel(); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index 34bc6b2f92..c862843a05 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -24,7 +24,6 @@ class TimerEventSourceTest { public static final int INITIAL_DELAY = 50; public static final int PERIOD = 50; - public static final int TESTING_TIME_SLACK = 40; private TimerEventSource timerEventSource; private CapturingEventHandler eventHandlerMock; @@ -38,35 +37,6 @@ public void setup() { timerEventSource.start(); } - @Test - public void producesEventsPeriodically() { - TestCustomResource customResource = TestUtils.testCustomResource(); - timerEventSource.schedule(customResource, INITIAL_DELAY, PERIOD); - - untilAsserted(() -> { - assertThat(eventHandlerMock.events) - .hasSizeGreaterThan(2); - assertThat(eventHandlerMock.events) - .allMatch(e -> e.getRelatedCustomResourceID() - .equals(CustomResourceID.fromResource(customResource))); - - }); - } - - @Test - public void deRegistersPeriodicalEventSources() { - TestCustomResource customResource = TestUtils.testCustomResource(); - - timerEventSource.schedule(customResource, INITIAL_DELAY, PERIOD); - untilAsserted(() -> assertThat(eventHandlerMock.events).hasSizeGreaterThan(1)); - - timerEventSource - .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); - - int size = eventHandlerMock.events.size(); - untilAsserted(() -> assertThat(eventHandlerMock.events).hasSize(size)); - } - @Test public void schedulesOnce() { TestCustomResource customResource = TestUtils.testCustomResource(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java index cecf713f3c..bc26fc3e47 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java @@ -2,9 +2,6 @@ import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.api.Context; @@ -24,9 +21,6 @@ public class EventSourceTestCustomResourceController public static final String FINALIZER_NAME = ControllerUtils.getDefaultFinalizerName( CustomResource.getCRDName(EventSourceTestCustomResource.class)); - private static final Logger log = - LoggerFactory.getLogger(EventSourceTestCustomResourceController.class); - public static final int TIMER_DELAY = 300; public static final int TIMER_PERIOD = 500; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private final TimerEventSource timerEventSource = @@ -41,13 +35,11 @@ public void prepareEventSources(EventSourceManager eventSourceManager) { public UpdateControl createOrUpdateResource( EventSourceTestCustomResource resource, Context context) { - timerEventSource.schedule(resource, TIMER_DELAY, TIMER_PERIOD); - numberOfExecutions.addAndGet(1); ensureStatusExists(resource); resource.getStatus().setState(EventSourceTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatusSubResource(resource).rescheduleAfter(TIMER_PERIOD); } private void ensureStatusExists(EventSourceTestCustomResource resource) { From 0ec51e0f762dd8d5e3521091349be75685f2a575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 10 Nov 2021 09:37:03 +0100 Subject: [PATCH 0125/1608] fix: cover informer automatic start case (#662) --- .../processing/event/AbstractEventSource.java | 2 +- .../event/internal/InformerEventSource.java | 39 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEventSource.java index eba4e7ac0a..8d9984db14 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEventSource.java @@ -2,7 +2,7 @@ public abstract class AbstractEventSource implements EventSource { - protected EventHandler eventHandler; + protected volatile EventHandler eventHandler; @Override public void setEventHandler(EventHandler eventHandler) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index d9252bb4c6..64b4a8d753 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -4,6 +4,9 @@ import java.util.Set; import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; @@ -16,35 +19,42 @@ public class InformerEventSource extends AbstractEventSource { + private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); + private final SharedInformer sharedInformer; - private final Function> resourceToUIDs; + private final Function> resourceToCustomResourceIDSet; private final Function associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToUIDs) { - this(sharedInformer, resourceToUIDs, null, true); + Function> resourceToCustomResourceIDSet) { + this(sharedInformer, resourceToCustomResourceIDSet, null, true); } public InformerEventSource(KubernetesClient client, Class type, - Function> resourceToUIDs) { - this(client, type, resourceToUIDs, false); + Function> resourceToCustomResourceIDSet) { + this(client, type, resourceToCustomResourceIDSet, false); } InformerEventSource(KubernetesClient client, Class type, - Function> resourceToUIDs, + Function> resourceToCustomResourceIDSet, boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToUIDs, null, + this(client.informers().sharedIndexInformerFor(type, 0), resourceToCustomResourceIDSet, null, skipUpdateEventPropagationIfNoChange); } public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToUIDs, + Function> resourceToCustomResourceIDSet, Function associatedWith, boolean skipUpdateEventPropagationIfNoChange) { this.sharedInformer = sharedInformer; - this.resourceToUIDs = resourceToUIDs; + this.resourceToCustomResourceIDSet = resourceToCustomResourceIDSet; this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; + if (sharedInformer.isRunning()) { + log.warn( + "Informer is already running on event source creation, this is not desirable and may " + + "lead to non deterministic behavior."); + } this.associatedWith = Objects.requireNonNullElseGet(associatedWith, () -> cr -> { final var metadata = cr.getMetadata(); @@ -76,13 +86,20 @@ public void onDelete(T t, boolean b) { } private void propagateEvent(T object) { - var uids = resourceToUIDs.apply(object); + var uids = resourceToCustomResourceIDSet.apply(object); if (uids.isEmpty()) { return; } uids.forEach(uid -> { Event event = new Event(CustomResourceID.fromResource(object)); - this.eventHandler.handleEvent(event); + /* + * In fabric8 client for certain cases informers can be created on in a way that they are + * automatically started, what would cause a NullPointerException here, since an event might + * be received between creation and registration. + */ + if (eventHandler != null) { + this.eventHandler.handleEvent(event); + } }); } From cd0d9a73b769cef24d999d54d16484a3e81e30a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 10 Nov 2021 12:47:16 +0100 Subject: [PATCH 0126/1608] docs: createOrUpdate vs delete (#663) --- docs/documentation/features.md | 40 ++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 9913529c58..d849b19a88 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -57,7 +57,7 @@ annotation for more details. ### When not to Use Finalizers? -Typically, automated finalizer handling should be turned off, when **all** the cleanup of the dependent resources is +Typically, automated finalizer handling should be turned off, in case **all** the cleanup of the dependent resources is handled by Kubernetes itself. This is handled by Kubernetes [garbage collection](https://kubernetes.io/docs/concepts/architecture/garbage-collection/#owners-dependents). Setting the owner reference and related fields are not in the scope of the SDK for now, it's up to the user to have them @@ -67,7 +67,43 @@ When automatic finalizer handling is turned off, the `ResourceController.deleteR case of a delete event received. So it does not make sense to implement this method and turn off finalizer at the same time. -## Separating `createOrUpdate` from `delete` +## The `createOrUpdateResource` and `deleteResource` Methods of `ResourceController` + +The lifecycle of a custom resource can be clearly separated to two phases from a perspective of an operator. +When a custom resource is created or update, or on the other hand when the custom resource is deleted - or rater +marked for deletion in case a finalizer is used. + +There is no point to make a distinction between create and update, since the reconciliation +logic typically would be very similar or identical in most of the cases. + +This separation related logic is automatically handled by framework. The framework will always call `createOrUpdateResource` +function, unless the custom resource is +[marked from deletion](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/#how-finalizers-work). +From the point when the custom resource is marked from deletion, only the `deleteResource` method is called. + +If there is **no finalizer** in place (see Finalizer Support section), the `deleteResource` method is **not called**. + +### Using `UpdateControl` and `DeleteControl` + +These two methods are used to control the outcome or the desired behavior after the reconciliation. + +The `UpdateControl` can instruct the framework to update the custom resource status sub-resource and/or re-schedule +a reconciliation with a desired time delay. Those are the typical use cases, however in some cases there it can happen +that the controller wants to update the custom resource itself (like adding annotations) or not to do any updates, +which are also supported. + +It is also possible to update both the status and the custom resource with `updateCustomResourceAndStatus` method. +In this case first the custom resource is updated then the status in two separate requests to K8S API. + +Always update the custom resource with `UpdateControl`, not with the actual kubernetes client if possible. + +The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent resource are +cleaned up in `deleteResource` implementation. + +However, there is a possibility to not remove the finalizer, this +allows to clean up the resources in a more async way, mostly for the cases when there is a long waiting period after a delete +operation is initiated. Note that in this case you might want to either schedule a timed event to make sure the +`deleteResource` is executed again or use event sources get notified about the state changes of a deleted resource. ## Automatic Retries on Error From 0563e0e43d6fac6bab9dcdb21ce92315b8eb13a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 10 Nov 2021 14:26:00 +0100 Subject: [PATCH 0127/1608] docs: retry + reschedule (#670) --- docs/documentation/features.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/documentation/features.md b/docs/documentation/features.md index d849b19a88..056cfffe1f 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -97,6 +97,9 @@ In this case first the custom resource is updated then the status in two separat Always update the custom resource with `UpdateControl`, not with the actual kubernetes client if possible. +On custom resource updates there is always an optimistic version control in place, to make sure that another update is +not overwritten (by setting `resourceVersion` ) . + The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent resource are cleaned up in `deleteResource` implementation. @@ -107,10 +110,36 @@ operation is initiated. Note that in this case you might want to either schedule ## Automatic Retries on Error -### Correctness and automatic retry +When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. +The retry is behavior is configurable, an implementation is provided that should cover most of the use-cases, see +[GenericRetry](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java) +But it is possible to provide a custom implementation. + +It is possible to set a limit on the number of retries. In the [Context](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java) +object information is provided about the retry, particularly interesting is the `isLastAttempt`, since a behavior +could be implemented bases on this flag. Like setting an error message in the status in case of a last attempt; + +Event if the retry reached a limit, in case of a new event is received the reconciliation would happen again, it's +just won't be a result of a retry, but the new event. + +A successful execution resets the retry. + +### Correctness and Automatic Retries + +There is a possibility to turn of the automatic retries. This is not desirable, unless there is a very specific +reason. Errors naturally happen, typically network errors can cause some temporal issues, another case is when a +custom resource is updated during the reconciliation (using `kubectl` for example), in this case +if an update of the custom resource from the controller (using `UpdateControl`) would fail on a conflict. The automatic +retries covers these cases and will result in a reconciliation, even if normally an event would not be processed +as a result of a custom resource update from previous example (like if there is no generation update as a result of the +change and generation filtering is turned on) ## Re-Scheduling Execution +In simple operators one way to implement an operator is to periodically reconcile it. This is supported explicitly by +`UpdateControl`, see method: `public UpdateControl withReSchedule(long delay, TimeUnit timeUnit)`. +This would schedule a reconciliation to the future. + ## Retry and Re-Scheduling Common Behavior ## Handling Related Events with Event Sources From 86245ca6d593abfc62d2dffea89f6b7d300652a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 10 Nov 2021 14:59:16 +0100 Subject: [PATCH 0128/1608] refactor: renaming core classes and APIs (#646) Renaming and refactoring structures based on the rational described in https://github.com/java-operator-sdk/java-operator-sdk/issues/655 --- .github/workflows/pr.yml | 2 +- DECISION_LOG.md | 12 -- README.md | 2 +- docs/documentation/features.md | 4 +- docs/etc/v1_model.drawio.svg | 4 + docs/etc/v2_model.drawio.svg | 4 + .../micrometer/MicrometerMetrics.java | 2 +- .../operator/ControllerUtils.java | 21 ++- .../io/javaoperatorsdk/operator/Operator.java | 36 ++--- .../operator/api/EventSourceInitializer.java | 16 -- .../operator/api/ObservedGenerationAware.java | 1 + .../config/AbstractConfigurationService.java | 10 +- .../api/config/ConfigurationService.java | 4 +- .../config/ConfigurationServiceOverrider.java | 4 +- .../api/config/ControllerConfiguration.java | 11 +- .../ControllerConfigurationOverrider.java | 2 +- .../DefaultControllerConfiguration.java | 2 +- .../operator/api/monitoring/Metrics.java | 2 +- .../api/{ => reconciler}/BaseControl.java | 2 +- .../api/{ => reconciler}/Context.java | 2 +- .../ControllerConfiguration.java} | 4 +- .../api/{ => reconciler}/DefaultContext.java | 2 +- .../api/{ => reconciler}/DeleteControl.java | 2 +- .../reconciler/EventSourceInitializer.java | 17 +++ .../Reconciler.java} | 8 +- .../api/{ => reconciler}/RetryInfo.java | 2 +- .../api/{ => reconciler}/UpdateControl.java | 2 +- ...figuredController.java => Controller.java} | 97 ++++++------ ...tEventHandler.java => EventProcessor.java} | 32 ++-- .../operator/processing/ExecutionScope.java | 2 +- ...her.java => ReconciliationDispatcher.java} | 30 ++-- .../event/DefaultEventSourceManager.java | 124 ---------------- .../processing/event/EventSourceManager.java | 138 ++++++++++++++++-- .../processing/event/EventSourceRegistry.java | 26 ++++ .../internal/CustomResourceEventSource.java | 9 +- .../processing/retry/RetryExecution.java | 2 +- .../operator/ControllerManagerTest.java | 24 +-- .../operator/ControllerUtilsTest.java | 8 +- .../operator/api/DeleteControlTest.java | 2 + .../config/ControllerConfigurationTest.java | 2 +- ...ndlerTest.java => EventProcessorTest.java} | 106 +++++++------- ...java => ReconciliationDispatcherTest.java} | 116 +++++++-------- ...rTest.java => EventSourceManagerTest.java} | 46 ++++-- .../CustomResourceEventFilterTest.java | 20 +-- .../CustomResourceEventSourceTest.java | 10 +- .../internal/CustomResourceSelectorTest.java | 23 +-- .../sample/simple/DuplicateCRController.java | 14 +- ...troller.java => TestCustomReconciler.java} | 23 ++- .../sample/simple/TestCustomReconcilerV2.java | 16 ++ .../TestCustomResourceControllerV2.java | 16 -- .../operator/junit/OperatorExtension.java | 28 ++-- .../runtime/AnnotationConfiguration.java | 42 +++--- ...llerConfigurationAnnotationProcessor.java} | 12 +- .../runtime/DefaultConfigurationService.java | 18 +-- .../runtime/RuntimeControllerMetadata.java | 14 +- .../operator/ConcurrencyIT.java | 8 +- .../operator/ControllerExecutionIT.java | 8 +- .../operator/EventSourceIT.java | 8 +- .../operator/InformerEventSourceIT.java | 8 +- .../io/javaoperatorsdk/operator/RetryIT.java | 12 +- .../operator/SubResourceUpdateIT.java | 6 +- .../operator/UpdatingResAndSubResIT.java | 7 +- ...ConfigurationAnnotationProcessorTest.java} | 20 +-- .../DefaultConfigurationServiceTest.java | 43 +++--- ... => DoubleUpdateTestCustomReconciler.java} | 18 +-- ...a => EventSourceTestCustomReconciler.java} | 24 +-- ...ormerEventSourceTestCustomReconciler.java} | 28 ++-- ...er.java => RetryTestCustomReconciler.java} | 18 +-- ...rceController.java => TestReconciler.java} | 25 ++-- ...a => SubResourceTestCustomReconciler.java} | 18 +-- ...ontroller.java => AbstractReconciler.java} | 6 +- .../AdditionalControllerInterface.java | 11 -- .../AdditionalReconcilerInterface.java | 11 ++ .../ControllerImplemented2Interfaces.java | 26 ---- ...rImplementedIntermediateAbstractClass.java | 23 --- ...java => MultilevelAbstractReconciler.java} | 4 +- .../MultilevelController.java | 28 ---- .../MultilevelReconciler.java | 28 ++++ .../ReconcilerImplemented2Interfaces.java | 26 ++++ ...rImplementedIntermediateAbstractClass.java | 23 +++ ...ller.java => CustomServiceReconciler.java} | 21 ++- .../sample/PureJavaApplicationRunner.java | 2 +- .../operator/sample/Config.java | 8 +- 83 files changed, 839 insertions(+), 809 deletions(-) delete mode 100644 DECISION_LOG.md create mode 100644 docs/etc/v1_model.drawio.svg create mode 100644 docs/etc/v2_model.drawio.svg delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/EventSourceInitializer.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{ => reconciler}/BaseControl.java (90%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{ => reconciler}/Context.java (65%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{Controller.java => reconciler/ControllerConfiguration.java} (95%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{ => reconciler}/DefaultContext.java (85%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{ => reconciler}/DeleteControl.java (93%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{ResourceController.java => reconciler/Reconciler.java} (91%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{ => reconciler}/RetryInfo.java (62%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{ => reconciler}/UpdateControl.java (97%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/{ConfiguredController.java => Controller.java} (68%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/{DefaultEventHandler.java => EventProcessor.java} (92%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/{EventDispatcher.java => ReconciliationDispatcher.java} (90%) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/{DefaultEventHandlerTest.java => EventProcessorTest.java} (75%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/{EventDispatcherTest.java => ReconciliationDispatcherTest.java} (70%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/{DefaultEventSourceManagerTest.java => EventSourceManagerTest.java} (53%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/{TestCustomResourceController.java => TestCustomReconciler.java} (81%) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerV2.java delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceControllerV2.java rename operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/{ControllerAnnotationProcessor.java => ControllerConfigurationAnnotationProcessor.java} (89%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/{ControllerAnnotationProcessorTest.java => ControllerConfigurationAnnotationProcessorTest.java} (71%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/{DoubleUpdateTestCustomResourceController.java => DoubleUpdateTestCustomReconciler.java} (71%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/{EventSourceTestCustomResourceController.java => EventSourceTestCustomReconciler.java} (58%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/{InformerEventSourceTestCustomResourceController.java => InformerEventSourceTestCustomReconciler.java} (66%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/{RetryTestCustomResourceController.java => RetryTestCustomReconciler.java} (77%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/{TestCustomResourceController.java => TestReconciler.java} (84%) rename operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/{SubResourceTestCustomResourceController.java => SubResourceTestCustomReconciler.java} (71%) rename operator-framework/src/test/resources/compile-fixtures/{AbstractController.java => AbstractReconciler.java} (58%) delete mode 100644 operator-framework/src/test/resources/compile-fixtures/AdditionalControllerInterface.java create mode 100644 operator-framework/src/test/resources/compile-fixtures/AdditionalReconcilerInterface.java delete mode 100644 operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java delete mode 100644 operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java rename operator-framework/src/test/resources/compile-fixtures/{MultilevelAbstractController.java => MultilevelAbstractReconciler.java} (58%) delete mode 100644 operator-framework/src/test/resources/compile-fixtures/MultilevelController.java create mode 100644 operator-framework/src/test/resources/compile-fixtures/MultilevelReconciler.java create mode 100644 operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java create mode 100644 operator-framework/src/test/resources/compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java rename samples/common/src/main/java/io/javaoperatorsdk/operator/sample/{CustomServiceController.java => CustomServiceReconciler.java} (73%) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a1ee4de399..936c95fb10 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -35,7 +35,7 @@ jobs: - name: Set up Minikube uses: manusa/actions-setup-minikube@v2.4.2 with: - minikube version: 'v1.22.0' + minikube version: 'v1.24.0' kubernetes version: ${{ matrix.kubernetes }} driver: 'docker' - name: Run integration tests diff --git a/DECISION_LOG.md b/DECISION_LOG.md deleted file mode 100644 index a81ad81d3b..0000000000 --- a/DECISION_LOG.md +++ /dev/null @@ -1,12 +0,0 @@ -# Decision Log - - -## Event Sources - -### 1. Move Retries to an Abstract Controller. - -The original idea was to explicitly support retry in the scheduler. However, this turned out to complicate the algorithm -in case of event sources. Mostly it would be harder to manage the buffer, and the other event sources, thus what -does it mean for other event sources that there was a failed controller execution? Probably it would be better to -manage this in an abstract controller, and just use the "reprocess event-source" source in case of an error. - diff --git a/README.md b/README.md index d42c45b7a0..790169d036 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ The Controller implements the business logic and describes all the classes neede ```java -@Controller +@ControllerConfiguration public class WebServerController implements ResourceController { // Return the changed resource, so it gets updated. See javadoc for details. diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 056cfffe1f..7e239d1768 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -45,14 +45,14 @@ Finalizers are automatically added by the framework as the first step, thus when before the first reconciliation, the custom resource is updated via a Kubernetes API call. As a result of this update, the finalizer will be present. The subsequent event will be received, which will trigger the first reconciliation. -The finalizer that is automatically added will be also removed after the `deleteResource` is executed on the controller. +The finalizer that is automatically added will be also removed after the `deleteResource` is executed on the controllerConfiguration. However, the removal behavior can be further customized, and can be instructed to "not remove yet" - this is useful just in some specific corner cases, when there would be a long waiting period for some dependent resource cleanup. The name of the finalizers can be specified, in case it is not, a name will be generated. This behavior can be turned off, so when configured no finalizer will be added or removed. -See [`@Controller`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java) +See [`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ControllerConfiguration.java) annotation for more details. ### When not to Use Finalizers? diff --git a/docs/etc/v1_model.drawio.svg b/docs/etc/v1_model.drawio.svg new file mode 100644 index 0000000000..162e573db2 --- /dev/null +++ b/docs/etc/v1_model.drawio.svg @@ -0,0 +1,4 @@ + + + +
Operator
Operator
ConfiguredController
ConfiguredController
DefaultEventSourceManager
DefaultEventSourceManager
DefaultEventHandler
DefaultEventHandler
EventDispatcher
EventDispatcher
ResourceController
ResourceController
EventSource
EventSource
EventHandler
EventHandler
1
1
1..*
1..*
1
1
1..*
1..*
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/etc/v2_model.drawio.svg b/docs/etc/v2_model.drawio.svg new file mode 100644 index 0000000000..9670b7d747 --- /dev/null +++ b/docs/etc/v2_model.drawio.svg @@ -0,0 +1,4 @@ + + + +
Operator
Operator
Controller
Controller
EventSourceManager
EventSourceManager
EventProcessor
EventProcessor
EventSource
EventSource
ReconcilationDispatcher
ReconcilationDispatcher
Reconciler
Reconciler
1
1
1..*
1..*
1..*
1..*
Label
Label
1
1
Viewer does not support full SVG 1.1
\ No newline at end of file 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 a90460d825..19f4f78bf7 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,8 +5,8 @@ import java.util.List; import java.util.Map; -import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.monitoring.Metrics; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; import io.micrometer.core.instrument.MeterRegistry; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java index b525055a81..cd3ca8f916 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java @@ -2,8 +2,8 @@ import java.util.Locale; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @SuppressWarnings("rawtypes") public class ControllerUtils { @@ -14,33 +14,32 @@ public static String getDefaultFinalizerName(String crdName) { return crdName + FINALIZER_NAME_SUFFIX; } - public static String getNameFor(Class controllerClass) { + public static String getNameFor(Class controllerClass) { // if the controller annotation has a name attribute, use it - final var annotation = controllerClass.getAnnotation(Controller.class); + final var annotation = controllerClass.getAnnotation(ControllerConfiguration.class); if (annotation != null) { final var name = annotation.name(); - if (!Controller.EMPTY_STRING.equals(name)) { + if (!ControllerConfiguration.EMPTY_STRING.equals(name)) { return name; } } - // otherwise, use the lower-cased full class name return getDefaultNameFor(controllerClass); } - public static String getNameFor(ResourceController controller) { + public static String getNameFor(Reconciler controller) { return getNameFor(controller.getClass()); } - public static String getDefaultNameFor(ResourceController controller) { + public static String getDefaultNameFor(Reconciler controller) { return getDefaultNameFor(controller.getClass()); } - public static String getDefaultNameFor(Class controllerClass) { - return getDefaultResourceControllerName(controllerClass.getSimpleName()); + public static String getDefaultNameFor(Class reconcilerClass) { + return getDefaultReconcilerName(reconcilerClass.getSimpleName()); } - public static String getDefaultResourceControllerName(String rcControllerClassName) { + public static String getDefaultReconcilerName(String rcControllerClassName) { // if the name is fully qualified, extract the simple class name final var lastDot = rcControllerClassName.lastIndexOf('.'); if (lastDot > 0) { 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 60fe670a36..405f4b6927 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 @@ -14,11 +14,11 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Version; import io.javaoperatorsdk.operator.api.LifecycleAware; -import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; -import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.processing.Controller; @SuppressWarnings("rawtypes") public class Operator implements AutoCloseable, LifecycleAware { @@ -49,7 +49,7 @@ public ConfigurationService getConfigurationService() { return configurationService; } - public List getControllers() { + public List getControllers() { return new ArrayList<>(controllers.controllers.values()); } @@ -114,7 +114,7 @@ public void close() { * @param the {@code CustomResource} type associated with the controller * @throws OperatorException if a problem occurred during the registration process */ - public > void register(ResourceController controller) + public > void register(Reconciler controller) throws OperatorException { register(controller, null); } @@ -126,29 +126,29 @@ public void close() { * passing it the controller's original configuration. The effective registration of the * controller is delayed till the operator is started. * - * @param controller the controller to register + * @param reconciler part of the controller to register * @param configuration the configuration with which we want to register the controller, if {@code * null}, the controller's original configuration is used * @param the {@code CustomResource} type associated with the controller * @throws OperatorException if a problem occurred during the registration process */ public > void register( - ResourceController controller, ControllerConfiguration configuration) + Reconciler reconciler, ControllerConfiguration configuration) throws OperatorException { - final var existing = configurationService.getConfigurationFor(controller); + final var existing = configurationService.getConfigurationFor(reconciler); if (existing == null) { throw new OperatorException( - "Cannot register controller with name " + controller.getClass().getCanonicalName() + - " controller named " + ControllerUtils.getNameFor(controller) + "Cannot register controller with name " + reconciler.getClass().getCanonicalName() + + " controller named " + ControllerUtils.getNameFor(reconciler) + " because its configuration cannot be found.\n" + " Known controllers are: " + configurationService.getKnownControllerNames()); } else { if (configuration == null) { configuration = existing; } - final var configuredController = - new ConfiguredController<>(controller, configuration, kubernetesClient); - controllers.add(configuredController); + final var controller = + new Controller<>(reconciler, configuration, kubernetesClient); + controllers.add(controller); final var watchedNS = configuration.watchAllNamespaces() @@ -163,7 +163,7 @@ public void close() { } static class ControllerManager implements LifecycleAware { - private final Map controllers = new HashMap<>(); + private final Map controllers = new HashMap<>(); private boolean started = false; public synchronized void shouldStart() { @@ -176,7 +176,7 @@ public synchronized void shouldStart() { } public synchronized void start() { - controllers.values().parallelStream().forEach(ConfiguredController::start); + controllers.values().parallelStream().forEach(Controller::start); started = true; } @@ -193,8 +193,8 @@ public synchronized void stop() { started = false; } - public synchronized void add(ConfiguredController configuredController) { - final var configuration = configuredController.getConfiguration(); + public synchronized void add(Controller controller) { + final var configuration = controller.getConfiguration(); final var crdName = configuration.getCRDName(); final var existing = controllers.get(crdName); if (existing != null) { @@ -202,9 +202,9 @@ public synchronized void add(ConfiguredController configuredController) { + "': another controller named '" + existing.getConfiguration().getName() + "' is already registered for CRD '" + crdName + "'"); } - this.controllers.put(crdName, configuredController); + this.controllers.put(crdName, controller); if (started) { - configuredController.start(); + controller.start(); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/EventSourceInitializer.java deleted file mode 100644 index e7500b9e47..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/EventSourceInitializer.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.javaoperatorsdk.operator.api; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.EventSourceManager; - -public interface EventSourceInitializer> { - - /** - * In this typically you might want to register event sources. But can access - * CustomResourceEventSource, what might be handy for some edge cases. - * - * @param eventSourceManager the {@link EventSourceManager} where event sources can be registered. - */ - void prepareEventSources(EventSourceManager eventSourceManager); - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java index 946fde48a3..e0a05a6b6c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; /** * If the custom resource's status implements this interface, the observed generation will be diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index af9c7856b8..b2eae5bbc2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -7,7 +7,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @SuppressWarnings("rawtypes") public class AbstractConfigurationService implements ConfigurationService { @@ -32,7 +32,7 @@ public AbstractConfigurationService(Version version) { if (failIfExisting) { final var existing = configurations.get(name); if (existing != null) { - throwExceptionOnNameCollision(config.getAssociatedControllerClassName(), existing); + throwExceptionOnNameCollision(config.getAssociatedReconcilerClassName(), existing); } } configurations.put(name, config); @@ -45,14 +45,14 @@ public AbstractConfigurationService(Version version) { "Controller name '" + existing.getName() + "' is used by both " - + existing.getAssociatedControllerClassName() + + existing.getAssociatedReconcilerClassName() + " and " + newControllerClassName); } @Override public > ControllerConfiguration getConfigurationFor( - ResourceController controller) { + Reconciler controller) { final var key = keyFor(controller); final var configuration = configurations.get(key); if (configuration == null) { @@ -73,7 +73,7 @@ private String getControllersNameMessage() { + "."; } - protected > String keyFor(ResourceController controller) { + protected > String keyFor(Reconciler controller) { return ControllerUtils.getNameFor(controller); } 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 b65115ec56..24e0605955 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 @@ -6,8 +6,8 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.monitoring.Metrics; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -37,7 +37,7 @@ public interface ConfigurationService { * null} if no configuration exists for the controller */ > ControllerConfiguration getConfigurationFor( - ResourceController controller); + Reconciler controller); /** * Retrieves the Kubernetes client configuration diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 62630f3ce2..3c223cd23f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -4,8 +4,8 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.monitoring.Metrics; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; public class ConfigurationServiceOverrider { private final ConfigurationService original; @@ -62,7 +62,7 @@ public ConfigurationService build() { return new ConfigurationService() { @Override public > ControllerConfiguration getConfigurationFor( - ResourceController controller) { + Reconciler controller) { return original.getConfigurationFor(controller); } 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 65ec26964b..70187b104c 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 @@ -6,14 +6,13 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Controller; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters; public interface ControllerConfiguration> { default String getName() { - return ControllerUtils.getDefaultResourceControllerName(getAssociatedControllerClassName()); + return ControllerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); } default String getCRDName() { @@ -45,7 +44,7 @@ default Class getCustomResourceClass() { return (Class) type.getActualTypeArguments()[0]; } - String getAssociatedControllerClassName(); + String getAssociatedReconcilerClassName(); default Set getNamespaces() { return Collections.emptySet(); @@ -66,7 +65,8 @@ default boolean watchCurrentNamespace() { static boolean currentNamespaceWatched(Set namespaces) { return namespaces != null && namespaces.size() == 1 - && namespaces.contains(Controller.WATCH_CURRENT_NAMESPACE); + && namespaces.contains( + io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.WATCH_CURRENT_NAMESPACE); } /** @@ -98,7 +98,8 @@ default RetryConfiguration getRetryConfiguration() { default void setConfigurationService(ConfigurationService service) {} default boolean useFinalizer() { - return !Controller.NO_FINALIZER.equals(getFinalizer()); + return !io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER + .equals(getFinalizer()); } /** 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 b9e4f9b7e6..006d707803 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 @@ -76,7 +76,7 @@ public ControllerConfigurationOverrider withCustomResourcePredicate( public ControllerConfiguration build() { return new DefaultControllerConfiguration<>( - original.getAssociatedControllerClassName(), + original.getAssociatedReconcilerClassName(), original.getName(), original.getCRDName(), finalizer, 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 index 672704f3c4..a0336eb92c 100644 --- 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 @@ -75,7 +75,7 @@ public boolean isGenerationAware() { } @Override - public String getAssociatedControllerClassName() { + public String getAssociatedReconcilerClassName() { return associatedControllerClassName; } 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 5544bc542e..efb99cc0ae 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 @@ -2,7 +2,7 @@ import java.util.Map; -import io.javaoperatorsdk.operator.api.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java similarity index 90% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java index aeca177cae..0fdcab7a56 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/BaseControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.api.reconciler; import java.util.Optional; import java.util.concurrent.TimeUnit; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java similarity index 65% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 3651414c16..bc8966f31c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.api.reconciler; import java.util.Optional; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java similarity index 95% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index bf03b03309..199a4985f8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.api.reconciler; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -9,7 +9,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) -public @interface Controller { +public @interface ControllerConfiguration { String EMPTY_STRING = ""; String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java similarity index 85% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index b74793d25c..8f73af29e7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.api.reconciler; import java.util.Optional; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java similarity index 93% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java index 0c2c3c87e5..5f41192c60 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/DeleteControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.api.reconciler; public class DeleteControl extends BaseControl { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java new file mode 100644 index 0000000000..f782b8bec7 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; + +public interface EventSourceInitializer> { + + /** + * In this typically you might want to register event sources. But can access + * CustomResourceEventSource, what might be handy for some edge cases. + * + * @param eventSourceRegistry the {@link EventSourceRegistry} where event sources can be + * registered. + */ + void prepareEventSources(EventSourceRegistry eventSourceRegistry); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java similarity index 91% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 7fe27ec908..937969126f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -1,8 +1,8 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.api.reconciler; import io.fabric8.kubernetes.client.CustomResource; -public interface ResourceController> { +public interface Reconciler> { /** * Note that this method is used in combination of finalizers. If automatic finalizer handling is @@ -28,7 +28,7 @@ public interface ResourceController> { * finalizer to indicate that the resource should not be deleted after all, in which case * the controller should restore the resource's state appropriately. */ - default DeleteControl deleteResource(R resource, Context context) { + default DeleteControl cleanup(R resource, Context context) { return DeleteControl.defaultDelete(); } @@ -46,6 +46,6 @@ default DeleteControl deleteResource(R resource, Context context) { * be skipped. However we will always call an update if there is no finalizer on object * and it's not marked for deletion. */ - UpdateControl createOrUpdateResource(R resource, Context context); + UpdateControl reconcile(R resource, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/RetryInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RetryInfo.java similarity index 62% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/RetryInfo.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RetryInfo.java index 92149012a7..2525bde192 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/RetryInfo.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RetryInfo.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.api.reconciler; public interface RetryInfo { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java similarity index 97% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index 2c9531ca06..7d7b6b6e0b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.api.reconciler; import io.fabric8.kubernetes.client.CustomResource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java similarity index 68% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index a22d135317..cb0cef3b90 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ConfiguredController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -11,34 +11,35 @@ import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.EventSourceInitializer; import io.javaoperatorsdk.operator.api.LifecycleAware; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; -import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +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.processing.event.EventSourceManager; +import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; -public class ConfiguredController> implements ResourceController, +public class Controller> implements Reconciler, LifecycleAware, EventSourceInitializer { - private final ResourceController controller; + private final Reconciler reconciler; private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; - private DefaultEventSourceManager eventSourceManager; + private EventSourceManager eventSourceManager; + private EventProcessor eventProcessor; - public ConfiguredController(ResourceController controller, + public Controller(Reconciler reconciler, ControllerConfiguration configuration, KubernetesClient kubernetesClient) { - this.controller = controller; + this.reconciler = reconciler; this.configuration = configuration; this.kubernetesClient = kubernetesClient; } @Override - public DeleteControl deleteResource(R resource, Context context) { + public DeleteControl cleanup(R resource, Context context) { return configuration.getConfigurationService().getMetrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -58,13 +59,13 @@ public String successTypeName(DeleteControl deleteControl) { @Override public DeleteControl execute() { - return controller.deleteResource(resource, context); + return reconciler.cleanup(resource, context); } }); } @Override - public UpdateControl createOrUpdateResource(R resource, Context context) { + public UpdateControl reconcile(R resource, Context context) { return configuration.getConfigurationService().getMetrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -91,13 +92,13 @@ public String successTypeName(UpdateControl result) { @Override public UpdateControl execute() { - return controller.createOrUpdateResource(resource, context); + return reconciler.reconcile(resource, context); } }); } @Override - public void prepareEventSources(EventSourceManager eventSourceManager) { + public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { throw new UnsupportedOperationException("This method should never be called directly"); } @@ -110,7 +111,7 @@ public boolean equals(Object o) { return false; } - ConfiguredController that = (ConfiguredController) o; + Controller that = (Controller) o; return configuration.getName().equals(that.configuration.getName()); } @@ -124,8 +125,8 @@ public String toString() { return "'" + configuration.getName() + "' Controller"; } - public ResourceController getController() { - return controller; + public Reconciler getReconciler() { + return reconciler; } public ControllerConfiguration getConfiguration() { @@ -153,35 +154,40 @@ public void start() throws OperatorException { final String controllerName = configuration.getName(); final var crdName = configuration.getCRDName(); final var specVersion = "v1"; - - // check that the custom resource is known by the cluster if configured that way - final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config - if (configuration.getConfigurationService().checkCRDAndValidateLocalModel()) { - crd = - kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName).get(); - if (crd == null) { - throwMissingCRDException(crdName, specVersion, controllerName); + try { + // check that the custom resource is known by the cluster if configured that way + final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config + if (configuration.getConfigurationService().checkCRDAndValidateLocalModel()) { + crd = + kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName) + .get(); + if (crd == null) { + throwMissingCRDException(crdName, specVersion, controllerName); + } + + // Apply validations that are not handled by fabric8 + CustomResourceUtils.assertCustomResource(resClass, crd); } - // Apply validations that are not handled by fabric8 - CustomResourceUtils.assertCustomResource(resClass, crd); - } - - try { - eventSourceManager = new DefaultEventSourceManager<>(this); - if (controller instanceof EventSourceInitializer) { - ((EventSourceInitializer) controller).prepareEventSources(eventSourceManager); + eventSourceManager = new EventSourceManager<>(this); + eventProcessor = + new EventProcessor<>(this, eventSourceManager.getCustomResourceEventSource()); + eventProcessor.setEventSourceManager(eventSourceManager); + eventSourceManager.setEventProcessor(eventProcessor); + if (reconciler instanceof EventSourceInitializer) { + ((EventSourceInitializer) reconciler).prepareEventSources(eventSourceManager); + } + if (failOnMissingCurrentNS()) { + throw new OperatorException( + "Controller '" + + controllerName + + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } + eventProcessor.start(); + eventSourceManager.start(); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); } - - if (failOnMissingCurrentNS()) { - throw new OperatorException( - "Controller '" - + controllerName - + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); - } } private void throwMissingCRDException(String crdName, String specVersion, String controllerName) { @@ -213,13 +219,12 @@ private boolean failOnMissingCurrentNS() { return false; } - public EventSourceManager getEventSourceManager() { - return eventSourceManager; - } - public void stop() { if (eventSourceManager != null) { eventSourceManager.stop(); } + if (eventProcessor != null) { + eventProcessor.stop(); + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java similarity index 92% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java index 9b5c10d877..1fd8952ebf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/DefaultEventHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java @@ -15,14 +15,14 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.LifecycleAware; -import io.javaoperatorsdk.operator.api.RetryInfo; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -36,13 +36,13 @@ * Event handler that makes sure that events are processed in a "single threaded" way per resource * UID, while buffering events which are received during an execution. */ -public class DefaultEventHandler> +public class EventProcessor> implements EventHandler, LifecycleAware { - private static final Logger log = LoggerFactory.getLogger(DefaultEventHandler.class); + private static final Logger log = LoggerFactory.getLogger(EventProcessor.class); private final Set underProcessing = new HashSet<>(); - private final EventDispatcher eventDispatcher; + private final ReconciliationDispatcher reconciliationDispatcher; private final Retry retry; private final Map retryState = new HashMap<>(); private final ExecutorService executor; @@ -51,29 +51,31 @@ public class DefaultEventHandler> private final Metrics metrics; private volatile boolean running; private final ResourceCache resourceCache; - private DefaultEventSourceManager eventSourceManager; + private EventSourceManager eventSourceManager; private final EventMarker eventMarker; - public DefaultEventHandler(ConfiguredController controller, ResourceCache resourceCache) { + public EventProcessor(Controller controller, ResourceCache resourceCache) { this( resourceCache, ExecutorServiceManager.instance().executorService(), controller.getConfiguration().getName(), - new EventDispatcher<>(controller), + new ReconciliationDispatcher<>(controller), GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), controller.getConfiguration().getConfigurationService().getMetrics(), new EventMarker()); } - DefaultEventHandler(EventDispatcher eventDispatcher, ResourceCache resourceCache, + EventProcessor(ReconciliationDispatcher reconciliationDispatcher, + ResourceCache resourceCache, String relatedControllerName, Retry retry, EventMarker eventMarker) { - this(resourceCache, null, relatedControllerName, eventDispatcher, retry, null, eventMarker); + this(resourceCache, null, relatedControllerName, reconciliationDispatcher, retry, null, + eventMarker); } - private DefaultEventHandler(ResourceCache resourceCache, ExecutorService executor, + private EventProcessor(ResourceCache resourceCache, ExecutorService executor, String relatedControllerName, - EventDispatcher eventDispatcher, Retry retry, Metrics metrics, + ReconciliationDispatcher reconciliationDispatcher, Retry retry, Metrics metrics, EventMarker eventMarker) { this.running = true; this.executor = @@ -82,14 +84,14 @@ private DefaultEventHandler(ResourceCache resourceCache, ExecutorService exec ConfigurationService.DEFAULT_RECONCILIATION_THREADS_NUMBER) : executor; this.controllerName = relatedControllerName; - this.eventDispatcher = eventDispatcher; + this.reconciliationDispatcher = reconciliationDispatcher; this.retry = retry; this.resourceCache = resourceCache; this.metrics = metrics != null ? metrics : Metrics.NOOP; this.eventMarker = eventMarker; } - public void setEventSourceManager(DefaultEventSourceManager eventSourceManager) { + public void setEventSourceManager(EventSourceManager eventSourceManager) { this.eventSourceManager = eventSourceManager; } @@ -359,7 +361,7 @@ public void run() { MDCUtils.addCustomResourceInfo(executionScope.getCustomResource()); thread.setName("EventHandler-" + controllerName); PostExecutionControl postExecutionControl = - eventDispatcher.handleExecution(executionScope); + reconciliationDispatcher.handleExecution(executionScope); eventProcessingFinished(executionScope, postExecutionControl); } finally { // restore original name diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java index 6cf05e9308..a24f3461c7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; public class ExecutionScope> { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java similarity index 90% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java index 3764667288..fa01bce577 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java @@ -8,36 +8,36 @@ import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.api.BaseControl; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.DefaultContext; -import io.javaoperatorsdk.operator.api.DeleteControl; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.BaseControl; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; /** - * Dispatches events to the Controller and handles Finalizers for a single type of Custom Resource. + * Handles calls and results of a Reconciler and finalizer related logic */ -public class EventDispatcher> { +public class ReconciliationDispatcher> { - private static final Logger log = LoggerFactory.getLogger(EventDispatcher.class); + private static final Logger log = LoggerFactory.getLogger(ReconciliationDispatcher.class); - private final ConfiguredController controller; + private final Controller controller; private final CustomResourceFacade customResourceFacade; - EventDispatcher(ConfiguredController controller, + ReconciliationDispatcher(Controller controller, CustomResourceFacade customResourceFacade) { this.controller = controller; this.customResourceFacade = customResourceFacade; } - public EventDispatcher(ConfiguredController controller) { + public ReconciliationDispatcher(Controller controller) { this(controller, new CustomResourceFacade<>(controller.getCRClient())); } @@ -85,7 +85,7 @@ private ControllerConfiguration configuration() { /** * Determines whether the given resource should be dispatched to the controller's - * {@link ResourceController#deleteResource(CustomResource, Context)} method + * {@link Reconciler#cleanup(CustomResource, Context)} method * * @param resource the resource to be potentially deleted * @return {@code true} if the resource should be handed to the controller's {@code @@ -115,7 +115,7 @@ private PostExecutionControl handleCreateOrUpdate( getVersion(resource), executionScope); - UpdateControl updateControl = controller.createOrUpdateResource(resource, context); + UpdateControl updateControl = controller.reconcile(resource, context); R updatedCustomResource = null; if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); @@ -173,7 +173,7 @@ private PostExecutionControl handleDelete(R resource, Context context) { getName(resource), getVersion(resource)); - DeleteControl deleteControl = controller.deleteResource(resource, context); + DeleteControl deleteControl = controller.cleanup(resource, context); final var useFinalizer = configuration().useFinalizer(); if (useFinalizer) { // note that we don't reschedule here even if instructed. Removing finalizer means that diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java deleted file mode 100644 index dd8e7f19ce..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManager.java +++ /dev/null @@ -1,124 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.locks.ReentrantLock; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.MissingCRDException; -import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.LifecycleAware; -import io.javaoperatorsdk.operator.processing.ConfiguredController; -import io.javaoperatorsdk.operator.processing.DefaultEventHandler; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; - -public class DefaultEventSourceManager> - implements EventSourceManager, LifecycleAware { - - private static final Logger log = LoggerFactory.getLogger(DefaultEventSourceManager.class); - - private final ReentrantLock lock = new ReentrantLock(); - private final Set eventSources = Collections.synchronizedSet(new HashSet<>()); - private DefaultEventHandler defaultEventHandler; - private TimerEventSource retryAndRescheduleTimerEventSource; - private CustomResourceEventSource customResourceEventSource; - - DefaultEventSourceManager(DefaultEventHandler defaultEventHandler) { - init(defaultEventHandler); - } - - public DefaultEventSourceManager(ConfiguredController controller) { - customResourceEventSource = new CustomResourceEventSource<>(controller); - init(new DefaultEventHandler<>(controller, customResourceEventSource)); - registerEventSource(customResourceEventSource); - } - - private void init(DefaultEventHandler defaultEventHandler) { - this.defaultEventHandler = defaultEventHandler; - defaultEventHandler.setEventSourceManager(this); - - this.retryAndRescheduleTimerEventSource = new TimerEventSource<>(); - registerEventSource(retryAndRescheduleTimerEventSource); - } - - @Override - public void start() throws OperatorException { - defaultEventHandler.start(); - } - - @Override - public void stop() { - lock.lock(); - try { - try { - defaultEventHandler.stop(); - } catch (Exception e) { - log.warn("Error closing event handler", e); - } - log.debug("Closing event sources."); - for (var eventSource : eventSources) { - try { - eventSource.stop(); - } catch (Exception e) { - log.warn("Error closing {} -> {}", eventSource, e); - } - } - eventSources.clear(); - } finally { - lock.unlock(); - } - } - - @Override - public final void registerEventSource(EventSource eventSource) - throws OperatorException { - Objects.requireNonNull(eventSource, "EventSource must not be null"); - lock.lock(); - try { - eventSources.add(eventSource); - eventSource.setEventHandler(defaultEventHandler); - eventSource.start(); - } catch (Throwable e) { - if (e instanceof IllegalStateException || e instanceof MissingCRDException) { - // leave untouched - throw e; - } - throw new OperatorException( - "Couldn't register event source: " + eventSource.getClass().getName(), e); - } finally { - lock.unlock(); - } - } - - public void cleanupForCustomResource(CustomResourceID customResourceUid) { - lock.lock(); - try { - for (EventSource eventSource : this.eventSources) { - eventSource.cleanupForCustomResource(customResourceUid); - } - } finally { - lock.unlock(); - } - } - - public TimerEventSource getRetryAndRescheduleTimerEventSource() { - return retryAndRescheduleTimerEventSource; - } - - @Override - public Set getRegisteredEventSources() { - return Collections.unmodifiableSet(eventSources); - } - - @Override - public CustomResourceEventSource getCustomResourceEventSource() { - return customResourceEventSource; - } - -} 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 e06ab2e3d1..b1d941d4e4 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,26 +1,138 @@ package io.javaoperatorsdk.operator.processing.event; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.LifecycleAware; +import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.EventProcessor; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; + +public class EventSourceManager> + implements EventSourceRegistry, LifecycleAware { + + private static final Logger log = LoggerFactory.getLogger(EventSourceManager.class); + + private final ReentrantLock lock = new ReentrantLock(); + private final Set eventSources = Collections.synchronizedSet(new HashSet<>()); + private EventProcessor eventProcessor; + private TimerEventSource retryAndRescheduleTimerEventSource; + private CustomResourceEventSource customResourceEventSource; + + EventSourceManager() { + init(); + } + + public EventSourceManager(Controller controller) { + init(); + customResourceEventSource = new CustomResourceEventSource<>(controller); + registerEventSource(customResourceEventSource); + } + + private void init() { + this.retryAndRescheduleTimerEventSource = new TimerEventSource<>(); + registerEventSource(retryAndRescheduleTimerEventSource); + } + + public EventSourceManager setEventProcessor(EventProcessor eventProcessor) { + this.eventProcessor = eventProcessor; + if (customResourceEventSource != null) { + customResourceEventSource.setEventHandler(eventProcessor); + } + if (retryAndRescheduleTimerEventSource != null) { + retryAndRescheduleTimerEventSource.setEventHandler(eventProcessor); + } + return this; + } + + @Override + public void start() throws OperatorException { + lock.lock(); + try { + log.debug("Starting event sources."); + for (var eventSource : eventSources) { + try { + eventSource.start(); + } catch (Exception e) { + log.warn("Error closing {} -> {}", eventSource, e); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public void stop() { + lock.lock(); + try { + log.debug("Closing event sources."); + for (var eventSource : eventSources) { + try { + eventSource.stop(); + } catch (Exception e) { + log.warn("Error closing {} -> {}", eventSource, e); + } + } + eventSources.clear(); + } finally { + lock.unlock(); + } + } + + @Override + public final void registerEventSource(EventSource eventSource) + throws OperatorException { + Objects.requireNonNull(eventSource, "EventSource must not be null"); + lock.lock(); + try { + eventSources.add(eventSource); + eventSource.setEventHandler(eventProcessor); + } catch (Throwable e) { + if (e instanceof IllegalStateException || e instanceof MissingCRDException) { + // leave untouched + throw e; + } + throw new OperatorException( + "Couldn't register event source: " + eventSource.getClass().getName(), e); + } finally { + lock.unlock(); + } + } -public interface EventSourceManager> { + public void cleanupForCustomResource(CustomResourceID customResourceUid) { + lock.lock(); + try { + for (EventSource eventSource : this.eventSources) { + eventSource.cleanupForCustomResource(customResourceUid); + } + } finally { + lock.unlock(); + } + } - /** - * Add the {@link EventSource} identified by the given name to the event manager. - * - * @param eventSource the {@link EventSource} to register - * @throws IllegalStateException if an {@link EventSource} with the same name is already - * registered. - * @throws OperatorException if an error occurred during the registration process - */ - void registerEventSource(EventSource eventSource) - throws IllegalStateException, OperatorException; + public TimerEventSource getRetryAndRescheduleTimerEventSource() { + return retryAndRescheduleTimerEventSource; + } - Set getRegisteredEventSources(); + @Override + public Set getRegisteredEventSources() { + return Collections.unmodifiableSet(eventSources); + } - CustomResourceEventSource getCustomResourceEventSource(); + @Override + public CustomResourceEventSource getCustomResourceEventSource() { + return customResourceEventSource; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java new file mode 100644 index 0000000000..95de04976e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.processing.event; + +import java.util.Set; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; + +public interface EventSourceRegistry> { + + /** + * Add the {@link EventSource} identified by the given name to the event manager. + * + * @param eventSource the {@link EventSource} to register + * @throws IllegalStateException if an {@link EventSource} with the same name is already + * registered. + * @throws OperatorException if an error occurred during the registration process + */ + void registerEventSource(EventSource eventSource) + throws IllegalStateException, OperatorException; + + Set getRegisteredEventSources(); + + CustomResourceEventSource getCustomResourceEventSource(); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 8845c599c6..c1391b92c7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -14,7 +14,7 @@ import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.ResourceCache; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; @@ -35,7 +35,7 @@ public class CustomResourceEventSource> extends A private static final Logger log = LoggerFactory.getLogger(CustomResourceEventSource.class); - private final ConfiguredController controller; + private final Controller controller; private final Map> sharedIndexInformers = new ConcurrentHashMap<>(); @@ -43,7 +43,7 @@ public class CustomResourceEventSource> extends A private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; private final Cloner cloner; - public CustomResourceEventSource(ConfiguredController controller) { + public CustomResourceEventSource(Controller controller) { this.controller = controller; this.cloner = controller.getConfiguration().getConfigurationService().getResourceCloner(); @@ -89,14 +89,13 @@ public void start() { }); } } catch (Exception e) { - // todo double check this if still applies for informers if (e instanceof KubernetesClientException) { KubernetesClientException ke = (KubernetesClientException) e; if (404 == ke.getCode()) { // only throw MissingCRDException if the 404 error occurs on the target CRD final var targetCRDName = controller.getConfiguration().getCRDName(); if (targetCRDName.equals(ke.getFullResourceName())) { - throw new MissingCRDException(targetCRDName, null); + throw new MissingCRDException(targetCRDName, null, e.getMessage(), e); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java index 9326d225d6..814a85b7d3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java @@ -2,7 +2,7 @@ import java.util.Optional; -import io.javaoperatorsdk.operator.api.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; public interface RetryExecution extends RetryInfo { 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 d25378499e..6db84b45e3 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 @@ -4,13 +4,13 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.Operator.ControllerManager; -import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; -import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.sample.simple.DuplicateCRController; +import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; +import io.javaoperatorsdk.operator.sample.simple.TestCustomReconcilerV2; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceControllerV2; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceV2; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -20,7 +20,7 @@ public class ControllerManagerTest { @Test public void shouldNotAddMultipleControllersForSameCustomResource() { - final var registered = new TestControllerConfiguration<>(new TestCustomResourceController(null), + final var registered = new TestControllerConfiguration<>(new TestCustomReconciler(null), TestCustomResource.class); final var duplicated = new TestControllerConfiguration<>(new DuplicateCRController(), TestCustomResource.class); @@ -30,9 +30,9 @@ public void shouldNotAddMultipleControllersForSameCustomResource() { @Test public void addingMultipleControllersForCustomResourcesWithDifferentVersionsShouldNotWork() { - final var registered = new TestControllerConfiguration<>(new TestCustomResourceController(null), + final var registered = new TestControllerConfiguration<>(new TestCustomReconciler(null), TestCustomResource.class); - final var duplicated = new TestControllerConfiguration<>(new TestCustomResourceControllerV2(), + final var duplicated = new TestControllerConfiguration<>(new TestCustomReconcilerV2(), TestCustomResourceV2.class); checkException(registered, duplicated); @@ -44,8 +44,8 @@ public void addingMultipleControllersForCustomResourcesWithDifferentVersionsShou TestControllerConfiguration duplicated) { final var exception = assertThrows(OperatorException.class, () -> { final var controllerManager = new ControllerManager(); - controllerManager.add(new ConfiguredController<>(registered.controller, registered, null)); - controllerManager.add(new ConfiguredController<>(duplicated.controller, duplicated, null)); + controllerManager.add(new Controller<>(registered.controller, registered, null)); + controllerManager.add(new Controller<>(duplicated.controller, duplicated, null)); }); final var msg = exception.getMessage(); assertTrue( @@ -56,16 +56,16 @@ public void addingMultipleControllersForCustomResourcesWithDifferentVersionsShou private static class TestControllerConfiguration> extends DefaultControllerConfiguration { - private final ResourceController controller; + private final Reconciler controller; - public TestControllerConfiguration(ResourceController controller, Class crClass) { + public TestControllerConfiguration(Reconciler controller, Class crClass) { super(null, getControllerName(controller), CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null); this.controller = controller; } static > String getControllerName( - ResourceController controller) { + Reconciler controller) { return controller.getClass().getSimpleName() + "Controller"; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java index cfc390f9c7..b8db81c2e9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.Test; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; +import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -11,8 +11,8 @@ class ControllerUtilsTest { @Test void getDefaultResourceControllerName() { assertEquals( - "testcustomresourcecontroller", - ControllerUtils.getDefaultResourceControllerName( - TestCustomResourceController.class.getCanonicalName())); + "testcustomreconciler", + ControllerUtils.getDefaultReconcilerName( + TestCustomReconciler.class.getCanonicalName())); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java index eb0fddd849..9907c0405e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/DeleteControlTest.java @@ -3,6 +3,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; + class DeleteControlTest { @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java index 7e404e0223..7f8698d81b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -12,7 +12,7 @@ class ControllerConfigurationTest { void getCustomResourceClass() { final ControllerConfiguration conf = new ControllerConfiguration<>() { @Override - public String getAssociatedControllerClassName() { + public String getAssociatedReconcilerClassName() { return null; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java similarity index 75% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java index 9be173e41b..2c10a37ddc 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/DefaultEventHandlerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java @@ -13,8 +13,8 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; -import io.javaoperatorsdk.operator.processing.event.DefaultEventSourceManager; import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; @@ -28,41 +28,43 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -class DefaultEventHandlerTest { +class EventProcessorTest { - private static final Logger log = LoggerFactory.getLogger(DefaultEventHandlerTest.class); + private static final Logger log = LoggerFactory.getLogger(EventProcessorTest.class); public static final int FAKE_CONTROLLER_EXECUTION_DURATION = 250; public static final int SEPARATE_EXECUTION_TIMEOUT = 450; public static final String TEST_NAMESPACE = "default-event-handler-test"; private EventMarker eventMarker = new EventMarker(); - private EventDispatcher eventDispatcherMock = mock(EventDispatcher.class); - private DefaultEventSourceManager defaultEventSourceManagerMock = - mock(DefaultEventSourceManager.class); + private ReconciliationDispatcher reconciliationDispatcherMock = + mock(ReconciliationDispatcher.class); + private EventSourceManager eventSourceManagerMock = + mock(EventSourceManager.class); private ResourceCache resourceCacheMock = mock(ResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); - private DefaultEventHandler defaultEventHandler = - new DefaultEventHandler(eventDispatcherMock, resourceCacheMock, "Test", null, eventMarker); + private EventProcessor eventProcessor = + new EventProcessor(reconciliationDispatcherMock, resourceCacheMock, "Test", null, + eventMarker); - private DefaultEventHandler defaultEventHandlerWithRetry = - new DefaultEventHandler(eventDispatcherMock, resourceCacheMock, "Test", + private EventProcessor eventProcessorWithRetry = + new EventProcessor(reconciliationDispatcherMock, resourceCacheMock, "Test", GenericRetry.defaultLimitedExponentialRetry(), eventMarker); @BeforeEach public void setup() { - when(defaultEventSourceManagerMock.getRetryAndRescheduleTimerEventSource()) + when(eventSourceManagerMock.getRetryAndRescheduleTimerEventSource()) .thenReturn(retryTimerEventSourceMock); - defaultEventHandler.setEventSourceManager(defaultEventSourceManagerMock); - defaultEventHandlerWithRetry.setEventSourceManager(defaultEventSourceManagerMock); + eventProcessor.setEventSourceManager(eventSourceManagerMock); + eventProcessorWithRetry.setEventSourceManager(eventSourceManagerMock); } @Test public void dispatchesEventsIfNoExecutionInProgress() { - defaultEventHandler.handleEvent(prepareCREvent()); + eventProcessor.handleEvent(prepareCREvent()); - verify(eventDispatcherMock, timeout(50).times(1)).handleExecution(any()); + verify(reconciliationDispatcherMock, timeout(50).times(1)).handleExecution(any()); } @Test @@ -71,18 +73,18 @@ public void skipProcessingIfLatestCustomResourceNotInCache() { when(resourceCacheMock.getCustomResource(event.getRelatedCustomResourceID())) .thenReturn(Optional.empty()); - defaultEventHandler.handleEvent(event); + eventProcessor.handleEvent(event); - verify(eventDispatcherMock, timeout(50).times(0)).handleExecution(any()); + verify(reconciliationDispatcherMock, timeout(50).times(0)).handleExecution(any()); } @Test public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedException { CustomResourceID resourceUid = eventAlreadyUnderProcessing(); - defaultEventHandler.handleEvent(nonCREvent(resourceUid)); + eventProcessor.handleEvent(nonCREvent(resourceUid)); - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) + verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) .handleExecution(any()); } @@ -94,7 +96,7 @@ public void schedulesAnEventRetryOnException() { PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); - defaultEventHandlerWithRetry.eventProcessingFinished(executionScope, postExecutionControl); + eventProcessorWithRetry.eventProcessingFinished(executionScope, postExecutionControl); verify(retryTimerEventSourceMock, times(1)) .scheduleOnce(eq(customResource), eq(GenericRetry.DEFAULT_INITIAL_INTERVAL)); @@ -108,18 +110,18 @@ public void executesTheControllerInstantlyAfterErrorIfNewEventsReceived() { PostExecutionControl postExecutionControl = PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); - when(eventDispatcherMock.handleExecution(any())) + when(reconciliationDispatcherMock.handleExecution(any())) .thenReturn(postExecutionControl) .thenReturn(PostExecutionControl.defaultDispatch()); // start processing an event - defaultEventHandlerWithRetry.handleEvent(event); + eventProcessorWithRetry.handleEvent(event); // handle another event - defaultEventHandlerWithRetry.handleEvent(event); + eventProcessorWithRetry.handleEvent(event); ArgumentCaptor executionScopeArgumentCaptor = ArgumentCaptor.forClass(ExecutionScope.class); - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) + verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) .handleExecution(executionScopeArgumentCaptor.capture()); List allValues = executionScopeArgumentCaptor.getAllValues(); assertThat(allValues).hasSize(2); @@ -138,23 +140,23 @@ public void successfulExecutionResetsTheRetry() { PostExecutionControl.exceptionDuringExecution(new RuntimeException("test")); PostExecutionControl defaultDispatchControl = PostExecutionControl.defaultDispatch(); - when(eventDispatcherMock.handleExecution(any())) + when(reconciliationDispatcherMock.handleExecution(any())) .thenReturn(postExecutionControlWithException) .thenReturn(defaultDispatchControl); ArgumentCaptor executionScopeArgumentCaptor = ArgumentCaptor.forClass(ExecutionScope.class); - defaultEventHandlerWithRetry.handleEvent(event); - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) + eventProcessorWithRetry.handleEvent(event); + verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) .handleExecution(any()); - defaultEventHandlerWithRetry.handleEvent(event); - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) + eventProcessorWithRetry.handleEvent(event); + verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) .handleExecution(any()); - defaultEventHandlerWithRetry.handleEvent(event); - verify(eventDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(3)) + eventProcessorWithRetry.handleEvent(event); + verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(3)) .handleExecution(executionScopeArgumentCaptor.capture()); log.info("Finished successfulExecutionResetsTheRetry"); @@ -170,10 +172,10 @@ public void successfulExecutionResetsTheRetry() { @Test public void scheduleTimedEventIfInstructedByPostExecutionControl() { var testDelay = 10000L; - when(eventDispatcherMock.handleExecution(any())) + when(reconciliationDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); - defaultEventHandler.handleEvent(prepareCREvent()); + eventProcessor.handleEvent(prepareCREvent()); verify(retryTimerEventSourceMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) .scheduleOnce(any(), eq(testDelay)); @@ -182,11 +184,11 @@ public void scheduleTimedEventIfInstructedByPostExecutionControl() { @Test public void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { var testDelay = 10000L; - when(eventDispatcherMock.handleExecution(any())) + when(reconciliationDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); - defaultEventHandler.handleEvent(prepareCREvent()); - defaultEventHandler.handleEvent(prepareCREvent()); + eventProcessor.handleEvent(prepareCREvent()); + eventProcessor.handleEvent(prepareCREvent()); verify(retryTimerEventSourceMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(0)) .scheduleOnce(any(), eq(testDelay)); @@ -194,10 +196,10 @@ public void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { @Test public void doNotFireEventsIfClosing() { - defaultEventHandler.stop(); - defaultEventHandler.handleEvent(prepareCREvent()); + eventProcessor.stop(); + eventProcessor.handleEvent(prepareCREvent()); - verify(eventDispatcherMock, timeout(50).times(0)).handleExecution(any()); + verify(reconciliationDispatcherMock, timeout(50).times(0)).handleExecution(any()); } @Test @@ -205,9 +207,9 @@ public void cleansUpWhenDeleteEventReceivedAndNoEventPresent() { Event deleteEvent = new CustomResourceEvent(DELETED, prepareCREvent().getRelatedCustomResourceID()); - defaultEventHandler.handleEvent(deleteEvent); + eventProcessor.handleEvent(deleteEvent); - verify(defaultEventSourceManagerMock, times(1)) + verify(eventSourceManagerMock, times(1)) .cleanupForCustomResource(eq(deleteEvent.getRelatedCustomResourceID())); } @@ -218,10 +220,10 @@ public void cleansUpAfterExecutionIfOnlyDeleteEventMarkLeft() { eventMarker.markDeleteEventReceived(crEvent.getRelatedCustomResourceID()); var executionScope = new ExecutionScope(cr, null); - defaultEventHandler.eventProcessingFinished(executionScope, + eventProcessor.eventProcessingFinished(executionScope, PostExecutionControl.defaultDispatch()); - verify(defaultEventSourceManagerMock, times(1)) + verify(eventSourceManagerMock, times(1)) .cleanupForCustomResource(eq(crEvent.getRelatedCustomResourceID())); } @@ -234,10 +236,10 @@ public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { var mockCREventSource = mock(CustomResourceEventSource.class); eventMarker.markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); - when(defaultEventSourceManagerMock.getCustomResourceEventSource()) + when(eventSourceManagerMock.getCustomResourceEventSource()) .thenReturn(mockCREventSource); - defaultEventHandler.eventProcessingFinished(new ExecutionScope(cr, null), + eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(updatedCr)); verify(mockCREventSource, times(1)).whitelistNextEvent(eq(crID)); @@ -254,10 +256,10 @@ public void dontWhitelistsEventWhenOtherChangeDuringExecution() { var mockCREventSource = mock(CustomResourceEventSource.class); eventMarker.markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(otherChangeCR)); - when(defaultEventSourceManagerMock.getCustomResourceEventSource()) + when(eventSourceManagerMock.getCustomResourceEventSource()) .thenReturn(mockCREventSource); - defaultEventHandler.eventProcessingFinished(new ExecutionScope(cr, null), + eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(updatedCr)); verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); @@ -270,10 +272,10 @@ public void dontWhitelistsEventIfUpdatedEventInCache() { var mockCREventSource = mock(CustomResourceEventSource.class); eventMarker.markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); - when(defaultEventSourceManagerMock.getCustomResourceEventSource()) + when(eventSourceManagerMock.getCustomResourceEventSource()) .thenReturn(mockCREventSource); - defaultEventHandler.eventProcessingFinished(new ExecutionScope(cr, null), + eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(cr)); verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); @@ -284,21 +286,21 @@ public void cancelScheduleOnceEventsOnSuccessfulExecution() { var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); - defaultEventHandler.eventProcessingFinished(new ExecutionScope(cr, null), + eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.defaultDispatch()); verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); } private CustomResourceID eventAlreadyUnderProcessing() { - when(eventDispatcherMock.handleExecution(any())) + when(reconciliationDispatcherMock.handleExecution(any())) .then( (Answer) invocationOnMock -> { Thread.sleep(FAKE_CONTROLLER_EXECUTION_DURATION); return PostExecutionControl.defaultDispatch(); }); Event event = prepareCREvent(); - defaultEventHandler.handleEvent(event); + eventProcessor.handleEvent(event); return event.getRelatedCustomResourceID(); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java similarity index 70% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java index 1c651afa60..05b4b68ff4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java @@ -10,15 +10,15 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.RetryInfo; -import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.EventDispatcher.CustomResourceFacade; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.ReconciliationDispatcher.CustomResourceFacade; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -35,49 +35,50 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -class EventDispatcherTest { +class ReconciliationDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; private TestCustomResource testCustomResource; - private EventDispatcher eventDispatcher; - private final ResourceController controller = mock(ResourceController.class); + private ReconciliationDispatcher reconciliationDispatcher; + private final Reconciler controller = mock(Reconciler.class); private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); private final ConfigurationService configService = mock(ConfigurationService.class); private final CustomResourceFacade customResourceFacade = - mock(EventDispatcher.CustomResourceFacade.class); + mock(ReconciliationDispatcher.CustomResourceFacade.class); @BeforeEach void setup() { testCustomResource = TestUtils.testCustomResource(); - eventDispatcher = init(testCustomResource, controller, configuration, customResourceFacade); + reconciliationDispatcher = + init(testCustomResource, controller, configuration, customResourceFacade); } - private > EventDispatcher init(R customResource, - ResourceController controller, ControllerConfiguration configuration, + private > ReconciliationDispatcher init(R customResource, + Reconciler reconciler, ControllerConfiguration configuration, CustomResourceFacade customResourceFacade) { when(configuration.getFinalizer()).thenReturn(DEFAULT_FINALIZER); when(configuration.useFinalizer()).thenCallRealMethod(); when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configService.getMetrics()).thenReturn(Metrics.NOOP); when(configuration.getConfigurationService()).thenReturn(configService); - when(controller.createOrUpdateResource(eq(customResource), any())) + when(reconciler.reconcile(eq(customResource), any())) .thenReturn(UpdateControl.updateCustomResource(customResource)); - when(controller.deleteResource(eq(customResource), any())) + when(reconciler.cleanup(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); when(customResourceFacade.replaceWithLock(any())).thenReturn(null); - ConfiguredController configuredController = - new ConfiguredController<>(controller, configuration, null); + Controller controller = + new Controller<>(reconciler, configuration, null); - return new EventDispatcher<>(configuredController, customResourceFacade); + return new ReconciliationDispatcher<>(controller, customResourceFacade); } @Test void addFinalizerOnNewResource() { assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, never()) - .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); + .reconcile(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) .replaceWithLock( argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); @@ -87,19 +88,19 @@ void addFinalizerOnNewResource() { @Test void callCreateOrUpdateOnNewResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, times(1)) - .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); + .reconcile(ArgumentMatchers.eq(testCustomResource), any()); } @Test void updatesOnlyStatusSubResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.createOrUpdateResource(eq(testCustomResource), any())) + when(controller.reconcile(eq(testCustomResource), any())) .thenReturn(UpdateControl.updateStatusSubResource(testCustomResource)); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); verify(customResourceFacade, never()).replaceWithLock(any()); @@ -109,11 +110,11 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { void updatesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.createOrUpdateResource(eq(testCustomResource), any())) + when(controller.reconcile(eq(testCustomResource), any())) .thenReturn(UpdateControl.updateCustomResourceAndStatus(testCustomResource)); when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, times(1)).replaceWithLock(testCustomResource); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); @@ -123,9 +124,9 @@ void updatesBothResourceAndStatusIfFinalizerSet() { void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(controller, times(1)) - .createOrUpdateResource(ArgumentMatchers.eq(testCustomResource), any()); + .reconcile(ArgumentMatchers.eq(testCustomResource), any()); } @Test @@ -134,9 +135,9 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { assertTrue(testCustomResource.addFinalizer(DEFAULT_FINALIZER)); markForDeletion(testCustomResource); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, times(1)).deleteResource(eq(testCustomResource), any()); + verify(controller, times(1)).cleanup(eq(testCustomResource), any()); } /** @@ -147,18 +148,18 @@ void callDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { configureToNotUseFinalizer(); markForDeletion(testCustomResource); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller).deleteResource(eq(testCustomResource), any()); + verify(controller).cleanup(eq(testCustomResource), any()); } @Test void doNotCallDeleteIfMarkedForDeletionWhenFinalizerHasAlreadyBeenRemoved() { markForDeletion(testCustomResource); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, never()).deleteResource(eq(testCustomResource), any()); + verify(controller, never()).cleanup(eq(testCustomResource), any()); } private void configureToNotUseFinalizer() { @@ -168,15 +169,16 @@ private void configureToNotUseFinalizer() { when(configService.getMetrics()).thenReturn(Metrics.NOOP); when(configuration.getConfigurationService()).thenReturn(configService); when(configuration.useFinalizer()).thenReturn(false); - eventDispatcher = new EventDispatcher(new ConfiguredController(controller, configuration, null), - customResourceFacade); + reconciliationDispatcher = + new ReconciliationDispatcher(new Controller(controller, configuration, null), + customResourceFacade); } @Test void doesNotAddFinalizerIfConfiguredNotTo() { configureToNotUseFinalizer(); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(0, testCustomResource.getMetadata().getFinalizers().size()); } @@ -186,7 +188,7 @@ void removesDefaultFinalizerOnDeleteIfSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(0, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, times(1)).replaceWithLock(any()); @@ -196,11 +198,11 @@ void removesDefaultFinalizerOnDeleteIfSet() { void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.deleteResource(eq(testCustomResource), any())) + when(controller.cleanup(eq(testCustomResource), any())) .thenReturn(DeleteControl.noFinalizerRemoval()); markForDeletion(testCustomResource); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, never()).replaceWithLock(any()); @@ -210,10 +212,10 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.createOrUpdateResource(eq(testCustomResource), any())) + when(controller.reconcile(eq(testCustomResource), any())) .thenReturn(UpdateControl.noUpdate()); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); verify(customResourceFacade, never()).updateStatus(testCustomResource); } @@ -221,10 +223,10 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { @Test void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); - when(controller.createOrUpdateResource(eq(testCustomResource), any())) + when(controller.reconcile(eq(testCustomResource), any())) .thenReturn(UpdateControl.noUpdate()); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(1, testCustomResource.getMetadata().getFinalizers().size()); verify(customResourceFacade, times(1)).replaceWithLock(any()); @@ -235,26 +237,26 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { removeFinalizers(testCustomResource); markForDeletion(testCustomResource); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); - verify(controller, never()).deleteResource(eq(testCustomResource), any()); + verify(controller, never()).cleanup(eq(testCustomResource), any()); } @Test void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, times(2)).createOrUpdateResource(eq(testCustomResource), any()); + verify(controller, times(2)).reconcile(eq(testCustomResource), any()); } @Test void propagatesRetryInfoToContextIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - eventDispatcher.handleExecution( + reconciliationDispatcher.handleExecution( new ExecutionScope( testCustomResource, new RetryInfo() { @@ -272,7 +274,7 @@ public boolean isLastAttempt() { ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(Context.class); verify(controller, times(1)) - .createOrUpdateResource(eq(testCustomResource), contextArgumentCaptor.capture()); + .reconcile(eq(testCustomResource), contextArgumentCaptor.capture()); Context context = contextArgumentCaptor.getValue(); final var retryInfo = context.getRetryInfo().get(); assertThat(retryInfo.getAttemptCount()).isEqualTo(2); @@ -283,12 +285,12 @@ public boolean isLastAttempt() { void setReScheduleToPostExecutionControlFromUpdateControl() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.createOrUpdateResource(eq(testCustomResource), any())) + when(controller.reconcile(eq(testCustomResource), any())) .thenReturn( UpdateControl.updateStatusSubResource(testCustomResource).rescheduleAfter(1000L)); PostExecutionControl control = - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); } @@ -298,11 +300,11 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(controller.deleteResource(eq(testCustomResource), any())) + when(controller.cleanup(eq(testCustomResource), any())) .thenReturn(DeleteControl.noFinalizerRemoval().rescheduleAfter(1000L)); PostExecutionControl control = - eventDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); } @@ -311,14 +313,14 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { void setObservedGenerationForStatusIfNeeded() { var observedGenResource = createObservedGenCustomResource(); - ResourceController lController = mock(ResourceController.class); + Reconciler lController = mock(Reconciler.class); ControllerConfiguration lConfiguration = mock(ControllerConfiguration.class); CustomResourceFacade lFacade = mock(CustomResourceFacade.class); var lDispatcher = init(observedGenResource, lController, lConfiguration, lFacade); when(lConfiguration.isGenerationAware()).thenReturn(true); - when(lController.createOrUpdateResource(eq(observedGenResource), any())) + when(lController.reconcile(eq(observedGenResource), any())) .thenReturn(UpdateControl.updateStatusSubResource(observedGenResource)); when(lFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java similarity index 53% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index d5b87bb5b5..76dc7e3411 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/DefaultEventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -3,11 +3,12 @@ import java.io.IOException; import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.processing.DefaultEventHandler; +import io.javaoperatorsdk.operator.processing.EventProcessor; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.eq; @@ -15,46 +16,63 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -class DefaultEventSourceManagerTest { +class EventSourceManagerTest { - private DefaultEventHandler defaultEventHandlerMock = mock(DefaultEventHandler.class); - private DefaultEventSourceManager defaultEventSourceManager = - new DefaultEventSourceManager(defaultEventHandlerMock); + private EventProcessor eventProcessorMock = mock(EventProcessor.class); + private EventSourceManager eventSourceManager = + new EventSourceManager(); + + @BeforeEach + public void setup() { + eventSourceManager.setEventProcessor(eventProcessorMock); + } @Test public void registersEventSource() { EventSource eventSource = mock(EventSource.class); - defaultEventSourceManager.registerEventSource(eventSource); + eventSourceManager.registerEventSource(eventSource); Set registeredSources = - defaultEventSourceManager.getRegisteredEventSources(); + eventSourceManager.getRegisteredEventSources(); assertThat(registeredSources).hasSize(2); - verify(eventSource, times(1)).setEventHandler(eq(defaultEventHandlerMock)); - verify(eventSource, times(1)).start(); + verify(eventSource, times(1)).setEventHandler(eq(eventProcessorMock)); } @Test public void closeShouldCascadeToEventSources() throws IOException { EventSource eventSource = mock(EventSource.class); EventSource eventSource2 = mock(EventSource.class); - defaultEventSourceManager.registerEventSource(eventSource); - defaultEventSourceManager.registerEventSource(eventSource2); + eventSourceManager.registerEventSource(eventSource); + eventSourceManager.registerEventSource(eventSource2); - defaultEventSourceManager.stop(); + eventSourceManager.stop(); verify(eventSource, times(1)).stop(); verify(eventSource2, times(1)).stop(); } + @Test + public void startCascadesToEventSources() { + EventSource eventSource = mock(EventSource.class); + EventSource eventSource2 = mock(EventSource.class); + eventSourceManager.registerEventSource(eventSource); + eventSourceManager.registerEventSource(eventSource2); + + eventSourceManager.start(); + + verify(eventSource, times(1)).start(); + verify(eventSource2, times(1)).start(); + } + @Test public void deRegistersEventSources() { CustomResource customResource = TestUtils.testCustomResource(); EventSource eventSource = mock(EventSource.class); - defaultEventSourceManager.registerEventSource(eventSource); + eventSourceManager.registerEventSource(eventSource); - defaultEventSourceManager + eventSourceManager .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); verify(eventSource, times(1)) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java index 86081e4af1..119d683880 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java @@ -15,7 +15,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; -import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -45,7 +45,7 @@ public void eventFilteredByCustomPredicate() { oldResource.getStatus().getConfigMapStatus(), newResource.getStatus().getConfigMapStatus())); - var controller = new TestConfiguredController(config); + var controller = new TestController(config); var eventSource = new CustomResourceEventSource<>(controller); eventSource.setEventHandler(eventHandler); @@ -73,7 +73,7 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { oldResource.getStatus().getConfigMapStatus(), newResource.getStatus().getConfigMapStatus())); - var controller = new TestConfiguredController(config); + var controller = new TestController(config); var eventSource = new CustomResourceEventSource<>(controller); eventSource.setEventHandler(eventHandler); @@ -103,7 +103,7 @@ public void observedGenerationFiltering() { when(config.getConfigurationService().getResourceCloner()) .thenReturn(ConfigurationService.DEFAULT_CLONER); - var controller = new ObservedGenConfiguredController(config); + var controller = new ObservedGenController(config); var eventSource = new CustomResourceEventSource<>(controller); eventSource.setEventHandler(eventHandler); @@ -134,7 +134,7 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { when(config.getConfigurationService().getResourceCloner()) .thenReturn(ConfigurationService.DEFAULT_CLONER); - var controller = new TestConfiguredController(config); + var controller = new TestController(config); var eventSource = new CustomResourceEventSource<>(controller); eventSource.setEventHandler(eventHandler); @@ -189,9 +189,9 @@ public ControllerConfig(String finalizer, boolean generationAware, } } - private static class TestConfiguredController extends ConfiguredController { + private static class TestController extends Controller { - public TestConfiguredController(ControllerConfiguration configuration) { + public TestController(ControllerConfiguration configuration) { super(null, configuration, null); } @@ -201,10 +201,10 @@ public MixedOperation { + private static class ObservedGenController + extends Controller { - public ObservedGenConfiguredController( + public ObservedGenController( ControllerConfiguration configuration) { super(null, configuration, null); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java index b8e4381f3b..61a550a652 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java @@ -13,7 +13,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -32,7 +32,7 @@ class CustomResourceEventSourceTest { EventHandler eventHandler = mock(EventHandler.class); private CustomResourceEventSource customResourceEventSource = - new CustomResourceEventSource<>(new TestConfiguredController(true)); + new CustomResourceEventSource<>(new TestController(true)); @BeforeEach public void setup() { @@ -89,7 +89,7 @@ public void normalExecutionIfGenerationChanges() { @Test public void handlesAllEventIfNotGenerationAware() { customResourceEventSource = - new CustomResourceEventSource<>(new TestConfiguredController(false)); + new CustomResourceEventSource<>(new TestController(false)); setup(); TestCustomResource customResource1 = TestUtils.testCustomResource(); @@ -136,9 +136,9 @@ public void notHandlesNextEventIfNotWhitelisted() { verify(eventHandler, times(0)).handleEvent(any()); } - private static class TestConfiguredController extends ConfiguredController { + private static class TestController extends Controller { - public TestConfiguredController(boolean generationAware) { + public TestController(boolean generationAware) { super(null, new TestConfiguration(generationAware), null); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index 7fcda72bb4..6a1e5144e0 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -22,13 +22,12 @@ import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Version; +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.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; @@ -121,7 +120,7 @@ void resourceWatchedByLabel() { NAMESPACE)); await() - .atMost(325, TimeUnit.SECONDS) + .atMost(5, TimeUnit.SECONDS) .pollInterval(100, TimeUnit.MILLISECONDS) .until(() -> c1.get() == 1 && c1err.get() == 0); await() @@ -145,7 +144,9 @@ public TestCustomResource newMyResource(String app, String namespace) { return resource; } - public static class MyConfiguration implements ControllerConfiguration { + public static class MyConfiguration + implements + io.javaoperatorsdk.operator.api.config.ControllerConfiguration { private final String labelSelector; private final ConfigurationService service; @@ -161,7 +162,7 @@ public String getLabelSelector() { } @Override - public String getAssociatedControllerClassName() { + public String getAssociatedReconcilerClassName() { return MyController.class.getCanonicalName(); } @@ -176,8 +177,8 @@ public ConfigurationService getConfigurationService() { } } - @Controller(namespaces = NAMESPACE) - public static class MyController implements ResourceController { + @ControllerConfiguration(namespaces = NAMESPACE) + public static class MyController implements Reconciler { private final Consumer consumer; @@ -186,7 +187,7 @@ public MyController(Consumer consumer) { } @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( TestCustomResource resource, Context context) { LOGGER.info("Received event on: {}", resource); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java index 443c4c21cc..86f0c36261 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java @@ -1,15 +1,15 @@ package io.javaoperatorsdk.operator.sample.simple; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +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; -@Controller -public class DuplicateCRController implements ResourceController { +@ControllerConfiguration +public class DuplicateCRController implements Reconciler { @Override - public UpdateControl createOrUpdateResource(TestCustomResource resource, + public UpdateControl reconcile(TestCustomResource resource, Context context) { return UpdateControl.noUpdate(); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java similarity index 81% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java index eba0859a9c..10b76b8cdc 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java @@ -11,16 +11,13 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -@Controller(generationAwareEventProcessing = false) -public class TestCustomResourceController implements ResourceController { +@ControllerConfiguration(generationAwareEventProcessing = false) +public class TestCustomReconciler implements Reconciler { - private static final Logger log = LoggerFactory.getLogger(TestCustomResourceController.class); + private static final Logger log = LoggerFactory.getLogger(TestCustomReconciler.class); public static final String CRD_NAME = CustomResource.getCRDName(TestCustomResource.class); public static final String FINALIZER_NAME = CRD_NAME + "/finalizer"; @@ -28,17 +25,17 @@ public class TestCustomResourceController implements ResourceController createOrUpdateResource( + public UpdateControl reconcile( TestCustomResource resource, Context context) { if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); @@ -84,7 +81,7 @@ public UpdateControl createOrUpdateResource( .createOrReplace(existingConfigMap); } else { Map labels = new HashMap<>(); - labels.put("managedBy", TestCustomResourceController.class.getSimpleName()); + labels.put("managedBy", TestCustomReconciler.class.getSimpleName()); ConfigMap newConfigMap = new ConfigMapBuilder() .withMetadata( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerV2.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerV2.java new file mode 100644 index 0000000000..bab22f309a --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerV2.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.simple; + +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; + +@ControllerConfiguration +public class TestCustomReconcilerV2 implements Reconciler { + + @Override + public UpdateControl reconcile(TestCustomResourceV2 resource, + Context context) { + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceControllerV2.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceControllerV2.java deleted file mode 100644 index 29a89381e2..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceControllerV2.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.javaoperatorsdk.operator.sample.simple; - -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; - -@Controller -public class TestCustomResourceControllerV2 implements ResourceController { - - @Override - public UpdateControl createOrUpdateResource(TestCustomResourceV2 resource, - Context context) { - return UpdateControl.noUpdate(); - } -} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index b48aca918b..5fed85dc67 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -26,11 +26,11 @@ import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.fabric8.kubernetes.client.utils.Utils; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Version; -import io.javaoperatorsdk.operator.processing.ConfiguredController; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.retry.Retry; import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override; @@ -106,16 +106,16 @@ public String getNamespace() { } @SuppressWarnings({"rawtypes"}) - public List getControllers() { + public List getControllers() { return operator.getControllers().stream() - .map(ConfiguredController::getController) + .map(Controller::getReconciler) .collect(Collectors.toUnmodifiableList()); } @SuppressWarnings({"rawtypes"}) - public T getControllerOfType(Class type) { + public T getControllerOfType(Class type) { return operator.getControllers().stream() - .map(ConfiguredController::getController) + .map(Controller::getReconciler) .filter(type::isInstance) .map(type::cast) .findFirst() @@ -167,6 +167,12 @@ protected void before(ExtensionContext context) { try (InputStream is = getClass().getResourceAsStream(path)) { kubernetesClient.load(is).createOrReplace(); + // this fixes an issue with CRD registration, integration tests were failing, since the CRD + // was not found yet + // when the operator started. This seems to be fixing this issue (maybe a problem with + // minikube?) + Thread.sleep(2000); + LOGGER.debug("Applied CRD with name: {}", config.getCRDName()); } catch (Exception ex) { throw new IllegalStateException("Cannot apply CRD yaml: " + path, ex); } @@ -242,19 +248,19 @@ public Builder withConfigurationService(ConfigurationService value) { } @SuppressWarnings("rawtypes") - public Builder withController(ResourceController value) { + public Builder withReconciler(Reconciler value) { controllers.add(new ControllerSpec(value, null)); return this; } @SuppressWarnings("rawtypes") - public Builder withController(ResourceController value, Retry retry) { + public Builder withReconciler(Reconciler value, Retry retry) { controllers.add(new ControllerSpec(value, retry)); return this; } @SuppressWarnings("rawtypes") - public Builder withController(Class value) { + public Builder withReconciler(Class value) { try { controllers.add(new ControllerSpec(value.getConstructor().newInstance(), null)); } catch (Exception e) { @@ -274,11 +280,11 @@ public OperatorExtension build() { @SuppressWarnings("rawtypes") private static class ControllerSpec { - final ResourceController controller; + final Reconciler controller; final Retry retry; public ControllerSpec( - ResourceController controller, + Reconciler controller, Retry retry) { this.controller = controller; this.retry = retry; diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 34ca56378f..4539873955 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -5,28 +5,27 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters; public class AnnotationConfiguration> - implements ControllerConfiguration { + implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { - private final ResourceController controller; - private final Controller annotation; + private final Reconciler reconciler; + private final ControllerConfiguration annotation; private ConfigurationService service; - public AnnotationConfiguration(ResourceController controller) { - this.controller = controller; - this.annotation = controller.getClass().getAnnotation(Controller.class); + public AnnotationConfiguration(Reconciler reconciler) { + this.reconciler = reconciler; + this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); } @Override public String getName() { - return ControllerUtils.getNameFor(controller); + return ControllerUtils.getNameFor(reconciler); } @Override @@ -40,22 +39,23 @@ public String getFinalizer() { @Override public boolean isGenerationAware() { - return valueOrDefault(annotation, Controller::generationAwareEventProcessing, true); + return valueOrDefault(annotation, ControllerConfiguration::generationAwareEventProcessing, + true); } @Override public Class getCustomResourceClass() { - return RuntimeControllerMetadata.getCustomResourceClass(controller); + return RuntimeControllerMetadata.getCustomResourceClass(reconciler); } @Override public Set getNamespaces() { - return Set.of(valueOrDefault(annotation, Controller::namespaces, new String[] {})); + return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, new String[] {})); } @Override public String getLabelSelector() { - return valueOrDefault(annotation, Controller::labelSelector, ""); + return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); } @Override @@ -69,8 +69,8 @@ public void setConfigurationService(ConfigurationService service) { } @Override - public String getAssociatedControllerClassName() { - return controller.getClass().getCanonicalName(); + public String getAssociatedReconcilerClassName() { + return reconciler.getClass().getCanonicalName(); } @SuppressWarnings("unchecked") @@ -79,7 +79,8 @@ public CustomResourceEventFilter getEventFilter() { CustomResourceEventFilter answer = null; Class>[] filterTypes = - (Class>[]) valueOrDefault(annotation, Controller::eventFilters, + (Class>[]) valueOrDefault(annotation, + ControllerConfiguration::eventFilters, new Object[] {}); if (filterTypes.length > 0) { for (var filterType : filterTypes) { @@ -101,12 +102,13 @@ public CustomResourceEventFilter getEventFilter() { : CustomResourceEventFilters.passthrough(); } - public static T valueOrDefault(Controller controller, Function mapper, + public static T valueOrDefault(ControllerConfiguration controllerConfiguration, + Function mapper, T defaultValue) { - if (controller == null) { + if (controllerConfiguration == null) { return defaultValue; } else { - return mapper.apply(controller); + return mapper.apply(controllerConfiguration); } } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessor.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java similarity index 89% rename from operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessor.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java index aaa55a01e8..df509f942e 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessor.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java @@ -16,17 +16,17 @@ import javax.lang.model.type.TypeMirror; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import com.google.auto.service.AutoService; import com.squareup.javapoet.TypeName; -import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.CONTROLLERS_RESOURCE_PATH; +import static io.javaoperatorsdk.operator.config.runtime.RuntimeControllerMetadata.RECONCILERS_RESOURCE_PATH; -@SupportedAnnotationTypes("io.javaoperatorsdk.operator.api.Controller") +@SupportedAnnotationTypes("io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration") @SupportedSourceVersion(SourceVersion.RELEASE_11) @AutoService(Processor.class) -public class ControllerAnnotationProcessor extends AbstractProcessor { +public class ControllerConfigurationAnnotationProcessor extends AbstractProcessor { private AccumulativeMappingWriter controllersResourceWriter; private TypeParameterResolver typeParameterResolver; @@ -35,7 +35,7 @@ public class ControllerAnnotationProcessor extends AbstractProcessor { public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); controllersResourceWriter = - new AccumulativeMappingWriter(CONTROLLERS_RESOURCE_PATH, processingEnv) + new AccumulativeMappingWriter(RECONCILERS_RESOURCE_PATH, processingEnv) .loadExistingMappings(); typeParameterResolver = initializeResolver(processingEnv); @@ -66,7 +66,7 @@ private TypeParameterResolver initializeResolver(ProcessingEnvironment processin .getDeclaredType( processingEnv .getElementUtils() - .getTypeElement(ResourceController.class.getCanonicalName()), + .getTypeElement(Reconciler.class.getCanonicalName()), processingEnv.getTypeUtils().getWildcardType(null, null)); return new TypeParameterResolver(resourceControllerType, 0); } 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 60e65a06f4..bcd2ed2f59 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 @@ -1,10 +1,10 @@ package io.javaoperatorsdk.operator.config.runtime; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ResourceController; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; public class DefaultConfigurationService extends BaseConfigurationService { @@ -20,27 +20,27 @@ public static DefaultConfigurationService instance() { @Override public > ControllerConfiguration getConfigurationFor( - ResourceController controller) { - return getConfigurationFor(controller, true); + Reconciler reconciler) { + return getConfigurationFor(reconciler, true); } > ControllerConfiguration getConfigurationFor( - ResourceController controller, boolean createIfNeeded) { - var config = super.getConfigurationFor(controller); + Reconciler reconciler, boolean createIfNeeded) { + var config = super.getConfigurationFor(reconciler); if (config == null) { if (createIfNeeded) { // create the configuration on demand and register it - config = new AnnotationConfiguration<>(controller); + config = new AnnotationConfiguration<>(reconciler); register(config); getLogger().info( "Created configuration for controller {} with name {}", - controller.getClass().getName(), + reconciler.getClass().getName(), config.getName()); } } else { // check that we don't have a controller name collision - final var newControllerClassName = controller.getClass().getCanonicalName(); - if (!config.getAssociatedControllerClassName().equals(newControllerClassName)) { + final var newControllerClassName = reconciler.getClass().getCanonicalName(); + if (!config.getAssociatedReconcilerClassName().equals(newControllerClassName)) { throwExceptionOnNameCollision(newControllerClassName, config); } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java index 1536d681f3..68ba58f1c1 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java @@ -3,29 +3,29 @@ import java.util.Map; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @SuppressWarnings("rawtypes") public class RuntimeControllerMetadata { - public static final String CONTROLLERS_RESOURCE_PATH = "javaoperatorsdk/controllers"; - private static final Map, Class> controllerToCustomResourceMappings; + public static final String RECONCILERS_RESOURCE_PATH = "javaoperatorsdk/reconcilers"; + private static final Map, Class> controllerToCustomResourceMappings; static { controllerToCustomResourceMappings = ClassMappingProvider.provide( - CONTROLLERS_RESOURCE_PATH, ResourceController.class, CustomResource.class); + RECONCILERS_RESOURCE_PATH, Reconciler.class, CustomResource.class); } static > Class getCustomResourceClass( - ResourceController controller) { + Reconciler reconciler) { final Class customResourceClass = - controllerToCustomResourceMappings.get(controller.getClass()); + controllerToCustomResourceMappings.get(reconciler.getClass()); if (customResourceClass == null) { throw new IllegalArgumentException( String.format( "No custom resource has been found for controller %s", - controller.getClass().getCanonicalName())); + reconciler.getClass().getCanonicalName())); } return (Class) customResourceClass; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index 4332faf413..458e2d12eb 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -13,7 +13,7 @@ import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; +import io.javaoperatorsdk.operator.sample.simple.TestReconciler; import io.javaoperatorsdk.operator.support.TestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -30,7 +30,7 @@ public class ConcurrencyIT { OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withController(new TestCustomResourceController(true)) + .withReconciler(new TestReconciler(true)) .build(); @Test @@ -48,7 +48,7 @@ public void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedExcepti List items = operator.resources(ConfigMap.class) .withLabel( - "managedBy", TestCustomResourceController.class.getSimpleName()) + "managedBy", TestReconciler.class.getSimpleName()) .list() .getItems(); assertThat(items).hasSize(NUMBER_OF_RESOURCES_CREATED); @@ -81,7 +81,7 @@ public void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedExcepti List items = operator.resources(ConfigMap.class) .withLabel( - "managedBy", TestCustomResourceController.class.getSimpleName()) + "managedBy", TestReconciler.class.getSimpleName()) .list() .getItems(); // reducing configmaps to names only - better for debugging diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index cb3cdbb704..b1f783b958 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -9,7 +9,7 @@ import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceController; +import io.javaoperatorsdk.operator.sample.simple.TestReconciler; import io.javaoperatorsdk.operator.support.TestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -20,12 +20,12 @@ public class ControllerExecutionIT { OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withController(new TestCustomResourceController(true)) + .withReconciler(new TestReconciler(true)) .build(); @Test public void configMapGetsCreatedForTestCustomResource() { - operator.getControllerOfType(TestCustomResourceController.class).setUpdateStatus(true); + operator.getControllerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); operator.create(TestCustomResource.class, resource); @@ -37,7 +37,7 @@ public void configMapGetsCreatedForTestCustomResource() { @Test public void eventIsSkippedChangedOnMetadataOnlyUpdate() { - operator.getControllerOfType(TestCustomResourceController.class).setUpdateStatus(false); + operator.getControllerOfType(TestReconciler.class).setUpdateStatus(false); TestCustomResource resource = TestUtils.testCustomResource(); operator.create(TestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index eb556535a1..509cb74d42 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -8,8 +8,8 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResource; -import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResourceController; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResourceSpec; import io.javaoperatorsdk.operator.support.TestUtils; @@ -21,7 +21,7 @@ public class EventSourceIT { OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withController(EventSourceTestCustomResourceController.class) + .withReconciler(EventSourceTestCustomReconciler.class) .build(); @Test @@ -33,7 +33,7 @@ public void receivingPeriodicEvents() { await() .atMost(5, TimeUnit.SECONDS) .pollInterval( - EventSourceTestCustomResourceController.TIMER_PERIOD / 2, TimeUnit.MILLISECONDS) + EventSourceTestCustomReconciler.TIMER_PERIOD / 2, TimeUnit.MILLISECONDS) .untilAsserted( () -> assertThat(TestUtils.getNumberOfExecutions(operator)) .isGreaterThanOrEqualTo(4)); @@ -45,7 +45,7 @@ public EventSourceTestCustomResource createTestCustomResource(String id) { new ObjectMetaBuilder() .withName("eventsource-" + id) .withNamespace(operator.getNamespace()) - .withFinalizers(EventSourceTestCustomResourceController.FINALIZER_NAME) + .withFinalizers(EventSourceTestCustomReconciler.FINALIZER_NAME) .build()); resource.setKind("Eventsourcesample"); resource.setSpec(new EventSourceTestCustomResourceSpec()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 1cb3fa7096..d290d06ecd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -10,11 +10,11 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResource; -import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResourceController; -import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResourceController.RELATED_RESOURCE_UID; -import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResourceController.TARGET_CONFIG_MAP_KEY; +import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.RELATED_RESOURCE_UID; +import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.TARGET_CONFIG_MAP_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -28,7 +28,7 @@ public class InformerEventSourceIT { OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withController(new InformerEventSourceTestCustomResourceController()) + .withReconciler(new InformerEventSourceTestCustomReconciler()) .build(); @Test diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index e5fcde6934..cb7109f8cf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -9,8 +9,8 @@ import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomReconciler; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResource; -import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResourceController; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResourceStatus; import io.javaoperatorsdk.operator.support.TestUtils; @@ -25,8 +25,8 @@ public class RetryIT { OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withController( - new RetryTestCustomResourceController(), + .withReconciler( + new RetryTestCustomReconciler(), new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry() .setMaxAttempts(5)) .build(); @@ -40,7 +40,7 @@ public void retryFailedExecution() { await("cr status updated") .pollDelay( - RETRY_INTERVAL * (RetryTestCustomResourceController.NUMBER_FAILED_EXECUTIONS + 2), + RETRY_INTERVAL * (RetryTestCustomReconciler.NUMBER_FAILED_EXECUTIONS + 2), TimeUnit.MILLISECONDS) .pollInterval( RETRY_INTERVAL, @@ -49,7 +49,7 @@ public void retryFailedExecution() { .untilAsserted(() -> { assertThat( TestUtils.getNumberOfExecutions(operator)) - .isEqualTo(RetryTestCustomResourceController.NUMBER_FAILED_EXECUTIONS + 1); + .isEqualTo(RetryTestCustomReconciler.NUMBER_FAILED_EXECUTIONS + 1); RetryTestCustomResource finalResource = operator.get(RetryTestCustomResource.class, @@ -64,7 +64,7 @@ public RetryTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("retrysource-" + id) - .withFinalizers(RetryTestCustomResourceController.FINALIZER_NAME) + .withFinalizers(RetryTestCustomReconciler.FINALIZER_NAME) .build()); resource.setKind("retrysample"); resource.setSpec(new RetryTestCustomResourceSpec()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 1cee81c054..5998502f32 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -9,8 +9,8 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResource; -import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceController; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResourceSpec; import io.javaoperatorsdk.operator.support.TestUtils; @@ -24,7 +24,7 @@ public class SubResourceUpdateIT { OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withController(SubResourceTestCustomResourceController.class) + .withReconciler(SubResourceTestCustomReconciler.class) .build(); @Test @@ -113,7 +113,7 @@ public SubResourceTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("subresource-" + id) - .withFinalizers(SubResourceTestCustomResourceController.FINALIZER_NAME) + .withFinalizers(SubResourceTestCustomReconciler.FINALIZER_NAME) .build()); resource.setKind("SubresourceSample"); resource.setSpec(new SubResourceTestCustomResourceSpec()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index 7d08ca6110..2a479d47df 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -8,8 +8,8 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomReconciler; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResource; -import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceController; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResourceStatus; import io.javaoperatorsdk.operator.support.TestUtils; @@ -22,7 +22,7 @@ public class UpdatingResAndSubResIT { OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withController(DoubleUpdateTestCustomResourceController.class) + .withReconciler(DoubleUpdateTestCustomReconciler.class) .build(); @Test @@ -47,7 +47,7 @@ public void updatesSubResourceStatus() { customResource .getMetadata() .getAnnotations() - .get(DoubleUpdateTestCustomResourceController.TEST_ANNOTATION)) + .get(DoubleUpdateTestCustomReconciler.TEST_ANNOTATION)) .isNotNull(); } @@ -73,7 +73,6 @@ void awaitStatusUpdated(String name) { public DoubleUpdateTestCustomResource createTestCustomResource(String id) { DoubleUpdateTestCustomResource resource = new DoubleUpdateTestCustomResource(); resource.setMetadata(new ObjectMetaBuilder().withName("doubleupdateresource-" + id).build()); - resource.setKind("DoubleUpdateSample"); resource.setSpec(new DoubleUpdateTestCustomResourceSpec()); resource.getSpec().setValue(id); return resource; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessorTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java similarity index 71% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessorTest.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java index e2f750b5d1..ce9637af9e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerAnnotationProcessorTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessorTest.java @@ -7,16 +7,16 @@ import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; -class ControllerAnnotationProcessorTest { +class ControllerConfigurationAnnotationProcessorTest { @Test public void generateCorrectDoneableClassIfInterfaceIsSecond() { Compilation compilation = Compiler.javac() - .withProcessors(new ControllerAnnotationProcessor()) + .withProcessors(new ControllerConfigurationAnnotationProcessor()) .compile( JavaFileObjects.forResource( - "compile-fixtures/ControllerImplemented2Interfaces.java")); + "compile-fixtures/ReconcilerImplemented2Interfaces.java")); CompilationSubject.assertThat(compilation).succeeded(); } @@ -24,11 +24,11 @@ public void generateCorrectDoneableClassIfInterfaceIsSecond() { public void generateCorrectDoneableClassIfThereIsAbstractBaseController() { Compilation compilation = Compiler.javac() - .withProcessors(new ControllerAnnotationProcessor()) + .withProcessors(new ControllerConfigurationAnnotationProcessor()) .compile( - JavaFileObjects.forResource("compile-fixtures/AbstractController.java"), + JavaFileObjects.forResource("compile-fixtures/AbstractReconciler.java"), JavaFileObjects.forResource( - "compile-fixtures/ControllerImplementedIntermediateAbstractClass.java")); + "compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java")); CompilationSubject.assertThat(compilation).succeeded(); } @@ -36,11 +36,11 @@ public void generateCorrectDoneableClassIfThereIsAbstractBaseController() { public void generateDoneableClasswithMultilevelHierarchy() { Compilation compilation = Compiler.javac() - .withProcessors(new ControllerAnnotationProcessor()) + .withProcessors(new ControllerConfigurationAnnotationProcessor()) .compile( - JavaFileObjects.forResource("compile-fixtures/AdditionalControllerInterface.java"), - JavaFileObjects.forResource("compile-fixtures/MultilevelAbstractController.java"), - JavaFileObjects.forResource("compile-fixtures/MultilevelController.java")); + JavaFileObjects.forResource("compile-fixtures/AdditionalReconcilerInterface.java"), + JavaFileObjects.forResource("compile-fixtures/MultilevelAbstractReconciler.java"), + JavaFileObjects.forResource("compile-fixtures/MultilevelReconciler.java")); CompilationSubject.assertThat(compilation).succeeded(); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 64623470ae..425f822ddc 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -12,10 +12,10 @@ import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Version; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +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 static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -74,10 +74,11 @@ void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { @Test public void returnsValuesFromControllerAnnotationFinalizer() { - final var controller = new TestCustomResourceController(); + final var reconciler = new TestCustomReconciler(); final var configuration = - DefaultConfigurationService.instance().getConfigurationFor(controller); - assertEquals(CustomResource.getCRDName(TestCustomResource.class), configuration.getCRDName()); + DefaultConfigurationService.instance().getConfigurationFor(reconciler); + assertEquals(CustomResource.getCRDName(TestCustomResource.class), + configuration.getCRDName()); assertEquals( ControllerUtils.getDefaultFinalizerName(configuration.getCRDName()), configuration.getFinalizer()); @@ -87,29 +88,29 @@ public void returnsValuesFromControllerAnnotationFinalizer() { @Test public void returnCustomerFinalizerNameIfSet() { - final var controller = new TestCustomFinalizerController(); + final var reconciler = new TestCustomFinalizerReconciler(); final var configuration = - DefaultConfigurationService.instance().getConfigurationFor(controller); + DefaultConfigurationService.instance().getConfigurationFor(reconciler); assertEquals(CUSTOM_FINALIZER_NAME, configuration.getFinalizer()); } @Test public void supportsInnerClassCustomResources() { - final var controller = new TestCustomFinalizerController(); + final var controller = new TestCustomFinalizerReconciler(); assertDoesNotThrow( () -> { DefaultConfigurationService.instance() .getConfigurationFor(controller) - .getAssociatedControllerClassName(); + .getAssociatedReconcilerClassName(); }); } - @Controller(finalizerName = CUSTOM_FINALIZER_NAME) - static class TestCustomFinalizerController - implements ResourceController { + @ControllerConfiguration(finalizerName = CUSTOM_FINALIZER_NAME) + static class TestCustomFinalizerReconciler + implements Reconciler { @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( InnerCustomResource resource, Context context) { return null; } @@ -120,23 +121,23 @@ public static class InnerCustomResource extends CustomResource { } } - @Controller(name = NotAutomaticallyCreated.NAME) - static class NotAutomaticallyCreated implements ResourceController { + @ControllerConfiguration(name = NotAutomaticallyCreated.NAME) + static class NotAutomaticallyCreated implements Reconciler { public static final String NAME = "should-be-logged"; @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( TestCustomResource resource, Context context) { return null; } } - @Controller(generationAwareEventProcessing = false, name = "test") - static class TestCustomResourceController implements ResourceController { + @ControllerConfiguration(generationAwareEventProcessing = false, name = "test") + static class TestCustomReconciler implements Reconciler { @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( TestCustomResource resource, Context context) { return null; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java similarity index 71% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java index df9f3dcf10..2f65be15a6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java @@ -6,24 +6,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +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.support.TestExecutionInfoProvider; -@Controller -public class DoubleUpdateTestCustomResourceController - implements ResourceController, TestExecutionInfoProvider { +@ControllerConfiguration +public class DoubleUpdateTestCustomReconciler + implements Reconciler, TestExecutionInfoProvider { private static final Logger log = - LoggerFactory.getLogger(DoubleUpdateTestCustomResourceController.class); + LoggerFactory.getLogger(DoubleUpdateTestCustomReconciler.class); public static final String TEST_ANNOTATION = "TestAnnotation"; public static final String TEST_ANNOTATION_VALUE = "TestAnnotationValue"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( DoubleUpdateTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java similarity index 58% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java index bc26fc3e47..c8a5c673c0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java @@ -4,18 +4,13 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.EventSourceInitializer; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; -import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -@Controller -public class EventSourceTestCustomResourceController - implements ResourceController, EventSourceInitializer, +@ControllerConfiguration +public class EventSourceTestCustomReconciler + implements Reconciler, TestExecutionInfoProvider { public static final String FINALIZER_NAME = @@ -23,16 +18,9 @@ public class EventSourceTestCustomResourceController CustomResource.getCRDName(EventSourceTestCustomResource.class)); public static final int TIMER_PERIOD = 500; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); - private final TimerEventSource timerEventSource = - new TimerEventSource<>(); @Override - public void prepareEventSources(EventSourceManager eventSourceManager) { - eventSourceManager.registerEventSource(timerEventSource); - } - - @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( EventSourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java similarity index 66% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index ad2a2dc863..a0b7805ccf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -5,29 +5,29 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.EventSourceInitializer; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +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.junit.KubernetesClientAware; -import io.javaoperatorsdk.operator.processing.event.EventSourceManager; +import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.internal.Mappers; -import static io.javaoperatorsdk.operator.api.Controller.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; /** * Copies the config map value from spec into status. The main purpose is to test and demonstrate * sample usage of InformerEventSource */ -@Controller(finalizerName = NO_FINALIZER) -public class InformerEventSourceTestCustomResourceController implements - ResourceController, KubernetesClientAware, +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class InformerEventSourceTestCustomReconciler implements + Reconciler, KubernetesClientAware, EventSourceInitializer { private static final Logger LOGGER = - LoggerFactory.getLogger(InformerEventSourceTestCustomResourceController.class); + LoggerFactory.getLogger(InformerEventSourceTestCustomReconciler.class); public static final String RELATED_RESOURCE_UID = "relatedResourceName"; public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; @@ -36,14 +36,14 @@ public class InformerEventSourceTestCustomResourceController implements private InformerEventSource eventSource; @Override - public void prepareEventSources(EventSourceManager eventSourceManager) { + public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { eventSource = new InformerEventSource<>(kubernetesClient, ConfigMap.class, Mappers.fromAnnotation(RELATED_RESOURCE_UID)); - eventSourceManager.registerEventSource(eventSource); + eventSourceRegistry.registerEventSource(eventSource); } @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( InformerEventSourceTestCustomResource resource, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java similarity index 77% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index cf24ee9484..5ae059342e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -7,15 +7,15 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +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.support.TestExecutionInfoProvider; -@Controller -public class RetryTestCustomResourceController - implements ResourceController, TestExecutionInfoProvider { +@ControllerConfiguration +public class RetryTestCustomReconciler + implements Reconciler, TestExecutionInfoProvider { public static final int NUMBER_FAILED_EXECUTIONS = 2; @@ -23,11 +23,11 @@ public class RetryTestCustomResourceController ControllerUtils.getDefaultFinalizerName( CustomResource.getCRDName(RetryTestCustomResource.class)); private static final Logger log = - LoggerFactory.getLogger(RetryTestCustomResourceController.class); + LoggerFactory.getLogger(RetryTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( RetryTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java similarity index 84% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index 6aefaa3922..30f57fd8c6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -13,20 +13,17 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -@Controller(generationAwareEventProcessing = false) -public class TestCustomResourceController - implements ResourceController, TestExecutionInfoProvider, +@ControllerConfiguration(generationAwareEventProcessing = false) +public class TestReconciler + implements Reconciler, TestExecutionInfoProvider, KubernetesClientAware { - private static final Logger log = LoggerFactory.getLogger(TestCustomResourceController.class); + private static final Logger log = LoggerFactory.getLogger(TestReconciler.class); public static final String FINALIZER_NAME = ControllerUtils.getDefaultFinalizerName(CustomResource.getCRDName(TestCustomResource.class)); @@ -35,11 +32,11 @@ public class TestCustomResourceController private KubernetesClient kubernetesClient; private boolean updateStatus; - public TestCustomResourceController() { + public TestReconciler() { this(true); } - public TestCustomResourceController(boolean updateStatus) { + public TestReconciler(boolean updateStatus) { this.updateStatus = updateStatus; } @@ -62,7 +59,7 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { } @Override - public DeleteControl deleteResource( + public DeleteControl cleanup( TestCustomResource resource, Context context) { Boolean delete = kubernetesClient @@ -85,7 +82,7 @@ public DeleteControl deleteResource( } @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( TestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { @@ -109,7 +106,7 @@ public UpdateControl createOrUpdateResource( .createOrReplace(existingConfigMap); } else { Map labels = new HashMap<>(); - labels.put("managedBy", TestCustomResourceController.class.getSimpleName()); + labels.put("managedBy", TestReconciler.class.getSimpleName()); ConfigMap newConfigMap = new ConfigMapBuilder() .withMetadata( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java similarity index 71% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index f04958cbcd..9eefc7568d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomResourceController.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -7,25 +7,25 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +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.support.TestExecutionInfoProvider; -@Controller(generationAwareEventProcessing = false) -public class SubResourceTestCustomResourceController - implements ResourceController, TestExecutionInfoProvider { +@ControllerConfiguration(generationAwareEventProcessing = false) +public class SubResourceTestCustomReconciler + implements Reconciler, TestExecutionInfoProvider { public static final String FINALIZER_NAME = ControllerUtils.getDefaultFinalizerName( CustomResource.getCRDName(SubResourceTestCustomResource.class)); private static final Logger log = - LoggerFactory.getLogger(SubResourceTestCustomResourceController.class); + LoggerFactory.getLogger(SubResourceTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( SubResourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { diff --git a/operator-framework/src/test/resources/compile-fixtures/AbstractController.java b/operator-framework/src/test/resources/compile-fixtures/AbstractReconciler.java similarity index 58% rename from operator-framework/src/test/resources/compile-fixtures/AbstractController.java rename to operator-framework/src/test/resources/compile-fixtures/AbstractReconciler.java index d35a18c070..6f351b845a 100644 --- a/operator-framework/src/test/resources/compile-fixtures/AbstractController.java +++ b/operator-framework/src/test/resources/compile-fixtures/AbstractReconciler.java @@ -1,12 +1,12 @@ package io; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import java.io.Serializable; -public abstract class AbstractController> implements Serializable, - ResourceController { +public abstract class AbstractReconciler> implements Serializable, + Reconciler { public static class MyCustomResource extends CustomResource { diff --git a/operator-framework/src/test/resources/compile-fixtures/AdditionalControllerInterface.java b/operator-framework/src/test/resources/compile-fixtures/AdditionalControllerInterface.java deleted file mode 100644 index 01077510f5..0000000000 --- a/operator-framework/src/test/resources/compile-fixtures/AdditionalControllerInterface.java +++ /dev/null @@ -1,11 +0,0 @@ -package io; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ResourceController; -import java.io.Serializable; - - -public interface AdditionalControllerInterface> extends - Serializable, - ResourceController { -} diff --git a/operator-framework/src/test/resources/compile-fixtures/AdditionalReconcilerInterface.java b/operator-framework/src/test/resources/compile-fixtures/AdditionalReconcilerInterface.java new file mode 100644 index 0000000000..2579e31c56 --- /dev/null +++ b/operator-framework/src/test/resources/compile-fixtures/AdditionalReconcilerInterface.java @@ -0,0 +1,11 @@ +package io; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import java.io.Serializable; + + +public interface AdditionalReconcilerInterface> extends + Serializable, + Reconciler { +} diff --git a/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java b/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java deleted file mode 100644 index 2b0fa58a8f..0000000000 --- a/operator-framework/src/test/resources/compile-fixtures/ControllerImplemented2Interfaces.java +++ /dev/null @@ -1,26 +0,0 @@ -package io; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; -import java.io.Serializable; - -@Controller -public class ControllerImplemented2Interfaces implements Serializable, ResourceController { - - public static class MyCustomResource extends CustomResource { - } - - @Override - public UpdateControl createOrUpdateResource(MyCustomResource customResource, Context context) { - return UpdateControl.updateCustomResource(null); - } - - @Override - public DeleteControl deleteResource(MyCustomResource customResource, Context context) { - return DeleteControl.defaultDelete(); - } -} diff --git a/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java b/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java deleted file mode 100644 index 5674a3ed81..0000000000 --- a/operator-framework/src/test/resources/compile-fixtures/ControllerImplementedIntermediateAbstractClass.java +++ /dev/null @@ -1,23 +0,0 @@ -package io; - -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.UpdateControl; -import java.io.Serializable; - -@Controller -public class ControllerImplementedIntermediateAbstractClass extends - AbstractController implements Serializable { - - public UpdateControl createOrUpdateResource( - AbstractController.MyCustomResource customResource, - Context context) { - return UpdateControl.updateCustomResource(null); - } - - public DeleteControl deleteResource(AbstractController.MyCustomResource customResource, - Context context) { - return DeleteControl.defaultDelete(); - } -} diff --git a/operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractController.java b/operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractReconciler.java similarity index 58% rename from operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractController.java rename to operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractReconciler.java index 4fe30adfe3..0dafba4c69 100644 --- a/operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractController.java +++ b/operator-framework/src/test/resources/compile-fixtures/MultilevelAbstractReconciler.java @@ -4,8 +4,8 @@ import java.io.Serializable; -public abstract class MultilevelAbstractController> implements +public abstract class MultilevelAbstractReconciler> implements Serializable, - AdditionalControllerInterface { + AdditionalReconcilerInterface { } diff --git a/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java b/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java deleted file mode 100644 index 0c07065337..0000000000 --- a/operator-framework/src/test/resources/compile-fixtures/MultilevelController.java +++ /dev/null @@ -1,28 +0,0 @@ -package io; - -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.UpdateControl; - -@Controller -public class MultilevelController extends - MultilevelAbstractController { - - public static class MyCustomResource extends CustomResource { - - } - - public UpdateControl createOrUpdateResource( - MultilevelController.MyCustomResource customResource, - Context context) { - return UpdateControl.updateCustomResource(null); - } - - public DeleteControl deleteResource(MultilevelController.MyCustomResource customResource, - Context context) { - return DeleteControl.defaultDelete(); - } - -} diff --git a/operator-framework/src/test/resources/compile-fixtures/MultilevelReconciler.java b/operator-framework/src/test/resources/compile-fixtures/MultilevelReconciler.java new file mode 100644 index 0000000000..fe15e54a6c --- /dev/null +++ b/operator-framework/src/test/resources/compile-fixtures/MultilevelReconciler.java @@ -0,0 +1,28 @@ +package io; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration +public class MultilevelReconciler extends + MultilevelAbstractReconciler { + + public static class MyCustomResource extends CustomResource { + + } + + public UpdateControl reconcile( + MultilevelReconciler.MyCustomResource customResource, + Context context) { + return UpdateControl.updateCustomResource(null); + } + + public DeleteControl cleanup(MultilevelReconciler.MyCustomResource customResource, + Context context) { + return DeleteControl.defaultDelete(); + } + +} diff --git a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java new file mode 100644 index 0000000000..6331c54d2c --- /dev/null +++ b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java @@ -0,0 +1,26 @@ +package io; + +import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import java.io.Serializable; + +@ControllerConfiguration +public class ReconcilerImplemented2Interfaces implements Serializable, Reconciler { + + public static class MyCustomResource extends CustomResource { + } + + @Override + public UpdateControl reconcile(MyCustomResource customResource, Context context) { + return UpdateControl.updateCustomResource(null); + } + + @Override + public DeleteControl cleanup(MyCustomResource customResource, Context context) { + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java new file mode 100644 index 0000000000..17280051a2 --- /dev/null +++ b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java @@ -0,0 +1,23 @@ +package io; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import java.io.Serializable; + +@ControllerConfiguration +public class ReconcilerImplementedIntermediateAbstractClass extends + AbstractReconciler implements Serializable { + + public UpdateControl reconcile( + AbstractReconciler.MyCustomResource customResource, + Context context) { + return UpdateControl.updateCustomResource(null); + } + + public DeleteControl cleanup(AbstractReconciler.MyCustomResource customResource, + Context context) { + return DeleteControl.defaultDelete(); + } +} diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java similarity index 73% rename from samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java rename to samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java index 932fd1bc1b..5400a8bef3 100644 --- a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceController.java +++ b/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java @@ -10,37 +10,34 @@ import io.fabric8.kubernetes.api.model.ServiceSpec; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.Context; -import io.javaoperatorsdk.operator.api.Controller; -import io.javaoperatorsdk.operator.api.DeleteControl; -import io.javaoperatorsdk.operator.api.ResourceController; -import io.javaoperatorsdk.operator.api.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; /** A very simple sample controller that creates a service with a label. */ -@Controller -public class CustomServiceController implements ResourceController { +@ControllerConfiguration +public class CustomServiceReconciler implements Reconciler { public static final String KIND = "CustomService"; - private static final Logger log = LoggerFactory.getLogger(CustomServiceController.class); + private static final Logger log = LoggerFactory.getLogger(CustomServiceReconciler.class); private final KubernetesClient kubernetesClient; - public CustomServiceController() { + public CustomServiceReconciler() { this(new DefaultKubernetesClient()); } - public CustomServiceController(KubernetesClient kubernetesClient) { + public CustomServiceReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } @Override - public DeleteControl deleteResource(CustomService resource, Context context) { + public DeleteControl cleanup(CustomService resource, Context context) { log.info("Execution deleteResource for: {}", resource.getMetadata().getName()); return DeleteControl.defaultDelete(); } @Override - public UpdateControl createOrUpdateResource( + public UpdateControl reconcile( CustomService resource, Context context) { log.info("Execution createOrUpdateResource for: {}", resource.getMetadata().getName()); diff --git a/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java b/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java index 6aaa737c7c..2cc212340b 100644 --- a/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java +++ b/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java @@ -11,7 +11,7 @@ public static void main(String[] args) { new Operator(ConfigurationServiceOverrider.override(DefaultConfigurationService.instance()) .withConcurrentReconciliationThreads(2) .build()); - operator.register(new CustomServiceController()); + operator.register(new CustomServiceReconciler()); operator.start(); } } diff --git a/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java b/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java index 40c5ecefb9..07dd4b50c0 100644 --- a/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java +++ b/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java @@ -6,20 +6,20 @@ import org.springframework.context.annotation.Configuration; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.ResourceController; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; @Configuration public class Config { @Bean - public CustomServiceController customServiceController() { - return new CustomServiceController(); + public CustomServiceReconciler customServiceController() { + return new CustomServiceReconciler(); } // Register all controller beans @Bean(initMethod = "start", destroyMethod = "stop") - public Operator operator(List controllers) { + public Operator operator(List controllers) { Operator operator = new Operator(DefaultConfigurationService.instance()); controllers.forEach(operator::register); return operator; From 1a48eb45760b6724d3457cc0eba60e5a924b343d Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 12 Nov 2021 16:22:41 +0100 Subject: [PATCH 0129/1608] chore(deps): upgrade to fabric8 5.10.1 (#656) Mocking the version is not needed anymore. (cherry picked from commit f3b9438a490c4ed8dc2ab0fbdffd0e683add375b) --- .../internal/CustomResourceSelectorTest.java | 23 +------------------ pom.xml | 2 +- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index 6a1e5144e0..ad741aa85d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -1,8 +1,5 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import java.text.ParseException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -18,7 +15,6 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.VersionInfo; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import io.javaoperatorsdk.operator.Operator; @@ -51,24 +47,7 @@ public class CustomResourceSelectorTest { @SuppressWarnings("unchecked") @BeforeEach - void setUpResources() throws ParseException { - String buildDate = - DateTimeFormatter.ofPattern(VersionInfo.VersionKeys.BUILD_DATE_FORMAT) - .format(LocalDateTime.now()); - - server - .expect() - .get() - .withPath("/version") - .andReturn( - 200, - new VersionInfo.Builder() - .withBuildDate(buildDate) - .withMajor("1") - .withMinor("21") - .build()) - .always(); - + void setUpResources() { configurationService = spy(ConfigurationService.class); when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); when(configurationService.getVersion()).thenReturn(new Version("1", "1", new Date())); diff --git a/pom.xml b/pom.xml index 9f29e7f03b..b788e40919 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ ${java.version} 5.8.1 - 5.8.0 + 5.10.1 1.7.32 2.14.1 4.0.0 From abc56bfd2d572e00940b1ba255a93da4c65825cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 12 Nov 2021 20:19:17 +0100 Subject: [PATCH 0130/1608] fix: startup bug, when CRs already present on cluster + renaming (#677) * chore(deps): upgrade to fabric8 5.10.1 (#656) Mocking the version is not needed anymore. (cherry picked from commit f3b9438a490c4ed8dc2ab0fbdffd0e683add375b) * fix: startup bug, when CRs already present on cluster + renaming * feat: pass selector as String instead of parsing it This is a new feature of Fabric8 5.9+ Co-authored-by: Chris Laprun --- .../internal/CustomResourceEventSource.java | 24 ++++++++++----- .../internal/LabelSelectorParserTest.java | 29 ------------------- .../operator/junit/OperatorExtension.java | 2 +- .../operator/support/TestUtils.java | 2 +- 4 files changed, 19 insertions(+), 38 deletions(-) delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParserTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index c1391b92c7..578c6894cb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -6,8 +6,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.informers.cache.Cache; @@ -23,7 +25,6 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; -import static io.javaoperatorsdk.operator.processing.event.internal.LabelSelectorParser.parseSimpleLabelSelector; /** * This is a special case since is not bound to a single custom resource @@ -74,16 +75,16 @@ public void start() { try { if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { - var informer = client.inAnyNamespace() - .withLabels(parseSimpleLabelSelector(labelSelector)).inform(this); - sharedIndexInformers.put(ANY_NAMESPACE_MAP_KEY, informer); + final var filteredBySelectorClient = client.inAnyNamespace() + .withLabelSelector(labelSelector); + final var informer = + createAndRunInformerFor(filteredBySelectorClient, ANY_NAMESPACE_MAP_KEY); log.debug("Registered {} -> {} for any namespace", controller, informer); } else { targetNamespaces.forEach( ns -> { - var informer = client.inNamespace(ns) - .withLabels(parseSimpleLabelSelector(labelSelector)).inform(this); - sharedIndexInformers.put(ns, informer); + final var informer = createAndRunInformerFor( + client.inNamespace(ns).withLabelSelector(labelSelector), ns); log.debug("Registered {} -> {} for namespace: {}", controller, informer, ns); }); @@ -103,6 +104,15 @@ public void start() { } } + private SharedIndexInformer createAndRunInformerFor( + FilterWatchListDeletable> filteredBySelectorClient, String key) { + var informer = filteredBySelectorClient.runnableInformer(0); + informer.addEventHandler(this); + sharedIndexInformers.put(key, informer); + informer.run(); + return informer; + } + @Override public void stop() { for (SharedIndexInformer informer : sharedIndexInformers.values()) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParserTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParserTest.java deleted file mode 100644 index 3a619bbc19..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParserTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.internal; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class LabelSelectorParserTest { - - @Test - public void nullParamReturnsEmptyMap() { - var res = LabelSelectorParser.parseSimpleLabelSelector(null); - assertThat(res).hasSize(0); - } - - @Test - public void emptyLabelSelectorReturnsEmptyMap() { - var res = LabelSelectorParser.parseSimpleLabelSelector(" "); - assertThat(res).hasSize(0); - } - - @Test - public void parseSimpleLabelSelector() { - var res = LabelSelectorParser.parseSimpleLabelSelector("app=foo"); - assertThat(res).hasSize(1).containsEntry("app", "foo"); - - res = LabelSelectorParser.parseSimpleLabelSelector("app=foo,owner=me"); - assertThat(res).hasSize(2).containsEntry("app", "foo").containsEntry("owner", "me"); - } -} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 5fed85dc67..ed19ed3ac2 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -106,7 +106,7 @@ public String getNamespace() { } @SuppressWarnings({"rawtypes"}) - public List getControllers() { + public List getReconcilers() { return operator.getControllers().stream() .map(Controller::getReconciler) .collect(Collectors.toUnmodifiableList()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java index a81cf9ce05..c2b5841d68 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/TestUtils.java @@ -58,6 +58,6 @@ public static void waitXms(int x) { } public static int getNumberOfExecutions(OperatorExtension extension) { - return ((TestExecutionInfoProvider) extension.getControllers().get(0)).getNumberOfExecutions(); + return ((TestExecutionInfoProvider) extension.getReconcilers().get(0)).getNumberOfExecutions(); } } From 2bb26440681c3e5c49ce2b599876d3fa4b56f307 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Nov 2021 09:36:42 +0100 Subject: [PATCH 0131/1608] chore(deps): bump junit from 4.13.1 to 4.13.2 (#679) Bumps [junit](https://github.com/junit-team/junit4) from 4.13.1 to 4.13.2. - [Release notes](https://github.com/junit-team/junit4/releases) - [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.13.1.md) - [Commits](https://github.com/junit-team/junit4/compare/r4.13.1...r4.13.2) --- updated-dependencies: - dependency-name: junit:junit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 6c7e6153844452ddc882b55468ebe7cd45f5e7b7 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 15 Nov 2021 15:24:05 +0100 Subject: [PATCH 0132/1608] chore: remove now unneeded class --- .../event/internal/LabelSelectorParser.java | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParser.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParser.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParser.java deleted file mode 100644 index 9c2061b642..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/LabelSelectorParser.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.internal; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -public class LabelSelectorParser { - - public static Map parseSimpleLabelSelector(String labelSelector) { - if (labelSelector == null || labelSelector.isBlank()) { - return Collections.EMPTY_MAP; - } - String[] selectors = labelSelector.split(","); - Map labels = new HashMap<>(selectors.length); - Arrays.stream(selectors).forEach(s -> { - var kv = s.split("="); - labels.put(kv[0], kv[1]); - }); - return labels; - } - -} From fce4e766c6de1828befe58b95409bb3eaa45cc3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 15 Nov 2021 16:05:15 +0100 Subject: [PATCH 0133/1608] docs: wip, event sources, eventing (#672) --- docs/assets/images/event-sources.png | Bin 0 -> 46120 bytes docs/documentation/features.md | 150 +++++++++++++++++---------- 2 files changed, 98 insertions(+), 52 deletions(-) create mode 100644 docs/assets/images/event-sources.png diff --git a/docs/assets/images/event-sources.png b/docs/assets/images/event-sources.png new file mode 100644 index 0000000000000000000000000000000000000000..773eaeb106e32f33d7047ad158bf8e5436237c27 GIT binary patch literal 46120 zcmYg&cRbZ^|38j#jKi^yL!5(w3MO>pIaUXY^5YSUNH?GL)f#&RH@tiYhWPavLNC z{E3OVu^Sm#5SgKl<~i?cOVxJg&O1D;J^ZU;b^6tl-E(8ND*Sv4^9y|o3oBh8e0t1I z#ZJYVJtM+J8=}LkD~*i7DLv)aq@rxMVAFMF(L@UhOdfUxaLlW|XI80WX7sh>>;7t| z?x(&#vR1Qv5fWLx$j@$~5~l{`oV5Bw5n`QsP;xlOlS1dj|&x zTUuJ?=H}AU(ms6nu(h?tDS6G^)3e5Z_ibh6{+E{*tE;Q;@*oh1(9qDv#zxG~u15;f z)6=Z1tOr;-sz#fll9Cbyo~JwlJdI`yU2#MVH8*qdR*)3qAc4A;s)JQt()^e?)qynW zIncr3ClMjHjEv&K!^1y*{P^O#l<*K_x#rF+wqn%GBN|50lf@^x0`~a zbGcE(E{(E3Yzv_rxtSFe6-DRt_4O}b=DxdnF%}VMieI$a2w^lDyN5WRgDR5R(f#Il z`*ttU&(BYm{YZCrw}gZQX$d?$JlxziR##UyH#gVU*Voq8e*doa-*wf{sQYq^0)cpQ zwUUF4jZKm2CoLhPwpKl2d%CW?fad+x3U}K3mCnRys|tIv%Q_b>T(JN4qd$LyLbFs# zzcrEX&Fj~5Z*>3PYNWV^eX(gxz`F|Q;63?u@M`#n{PsD*4H#=-Eo=k#)Z?-;1%AF< zqO-HJy}iAT4&`Hk1c6`yDJjz)=Fh?W46N+z>|9*YmT?}3dmeG!A;;-oA`8Us@nZx8 zZM2qokYA!+a7tX!KxFtAJ4+y6+7swsw(ulTFHlsG*RCSqB~d8U>C>l=Fbbg;G?FD~ zxEborvvCFFzmc|7gixyp@-uE3ZsyL}s0%#yENPgpH@fPTyIl#!)~Fk`4WdDYHWl{V zmkQc*>T{kykFX%tYUt_Btpxtv@6R{hy_HO=Jsk~(zM$;zgYw$iKq8T7YisN8|5zPe zB7&3{E&eG7H3gLgH6r)MvUd5`FL;=-x7SbIy{j5%^v^1hOBofzcy(7-*8#}m*s|`w0p`)k2Zfkq#^5tEse@dOgI*D3FrnDnn za)Tb({c{=*~`IVKAEdM!g{5Wcd^16CSoFJ1y zXmGHmu`yF^(0^BDVtILaYN`SLGI46kI)xg3oAr2UI7d)BD9IF-A5n{{<$wQ)&=w{PE`$yC(7OSf14QTiIOriU*+WujE{Fq3JB5~XhU!L@IgM4 zl^^zIE)tW6S68>x)XaJHDjwE@B`Kn<&EWLuuI}zL0bHae8C9qCj9de2d;R)lQn`c) z*5_%SIu)B2411Kp2Q3mdhKuXL+}w=-a=f7iwxOW`hjQPhg`bsDZ^EIVZv3{FZ{NH* z#feU#%)<)`>X@0O{_n@v*Ikbtdl3;q{qK*5goff#lGmyx(n$*thJ>{d6&3Yd8NYh* zB5A*Aw^YymuMR|yAAgpYck|}W9e6NuhjPzf57W{(3=#kR@I*fK2w4@stvE{!0~QL0 zY=xWu>;|lg$@%j~p?gj}pbmQt{W&_CHXzts^DtH>=Q<^a0UVQme~A;kfpL~vxNMa7 zbMgpgPR^m>;hniI?tlk7LB}<$Vf*#;^>?ptTz?$;-D{UDyu$al=>L8ajzJQ20it%| zHYE*B;NkBfZh6O^!9hk^+In+n;|IYU40-j9joN3> zz@e|cek-)&wgbDtqN^(AY;Ba|YSQukTKusp85C{`Lf6_ZB zdM*twk5xj4>eaU&rjk-o@t>#*whTNxujAmoG1pb?w>?2FW2kLzKK$YKH+U*uRbMtN zmd^0&+v^=;m0sVKRebri8nsM*U8X^2MjzXul9`^Lx3jX6I$UJ>XB|s4UOG3F4CTca&`VcV>|Xbbj6_FUi`848+@T(e3JQ?%{x6NL zRC)Jyb~;wb5@U?83-L)%@=XWOE@kEAwXix${dI`s-@_#~OSu|1ZU|ZXyShqOSmF2f z_F&sC*9IK^{y8L7C&;-0-J|;EmqMr7!wVNLmQ_?>lTS!Xzbz^wkoRNg z*BKd6B9(XdJpq`+eHj{>|MjcdhInC`k(HKnmts#lHKR@5p-gIyPWRi3H&!0cYNgp4dj97p{+Ggl=Gc&W^ z2Frk5Dnw*h*yh$&1KH~U#^Vj zR(PW125pXZBZ@YM>3KNE8CO@gUti^n>}NmScS8Nmup9{7+x$^>5;3QxuWz?!{!jO# zgQ*o(CZM075^#`VxhlcK!-LMV@NRtb=+U?KRAFWlXyZdg7OYTkoSHRiDk>`NnJU9& zPTyAowokxGi@Q!FCOvslmcdlyyvu zJc?*m){SOP%Y%OPL)MBMA><@H(Al$R3kWJIJojy4yjI4WXR^h%m&dp`5^zS`dd zH(B<(cgCxtKNgpj>F-N{i==d`r6_MH(ZZi1P8-m6#Q z5mg>PIU<73)|C-DpxrjaKb|?$Vs2q!H317&$(&N-Y6xwwlS#@z8vxtdbn84G)%KhD z$_P89PGwL~(5uU3dU|^N3$FpNjR%!4{QTK5<#qMynEIbJCfil9)A|f8tAPj8e9Y@s zOffMryu7?8yq4b;7YC_&`}iDAG6j}9Pf}3iy?ZBHWIa~lQGj~kfBH140kJ;yJoPfq)oFs6rB3ubEjy`gVuJo(b4EIxJb9A;WFzJF*P~4z zhTd2TI0~dDBqk;%Bnt(HB<9!_sc~~(u*a=#?puI zZ&Kj@2L9a+oSvFO<9;nIp$C||`PBT)FI=ED)#x&zoH<;G9#xf+VrIR0>z3c21Et(b z=*zisEZdEdOr^LO!;ZE#I3<6ce>B7CLz`0NSNO_AtFd~))U;mAhQ*%Y3SLZ%A%Gw) zEiEe>77b8&ZhpQIkmQD4wevvcQ~j*}n( z(iHj+uIkwF;l#KaKcoM0K|miHUEkviI#3uFss^Mc-)TBq+d58gW4w?0wzD}4EK#L`_vRFs;A z=BX+T6R-c?<}ulV+S;tK_HwV4YwzkmeWKggf%CSrvjc^B``ZW9rBI6q_V;CFjaNH6 zJ8|Up&c42Tb#-;XHhKx?8iaxin0o8Y?bV-OcXA4x?>-_%PD2y9ynM%qF(5SU;<lcus6lwdL$g-rlrzX*LNqNUN(v6mc2bL?NLTXd3iZ* zf&ZA~@#F9(=qsEWw_#3Ssq_?&&>U5TX<6a%6GsWGvaRjs&!5b9m>)5-jd0O(Nak?D z>1dmqotf#*QcqNj$$s?k;oj<`c2iqc;GYL$VzhE24aq4f(%sdRwC77`ShMLd-p>^b z8mm$!+uG8? ze;`2I5bEMr&@Qia{;|o}Y2e`@XG?Woo9*?r>KO0QzP^SVsa-!d>GHR&RKq!=oLe#5 zgER+>D3s?|n~eoh;`AzsMwE$`HrOuvy?G&j!0|8=xuA^F$I8me z>LgS5YO?W6p#__I!d3mYaDsn5HB7C%xI5kXtpbo*a`f|6UZ-MN+H}J%emR(<>O*sI zP*(%zMKUrb#Ko~)p~L_8$N)mKcW_Yk-HfutU4RkI#?GF66xdX6kZc$%EC#~{KT1gn zwP|Z-7u)LyKNrQsD`n6a%PBctCR=Q_4T$9E(W7khb2J>l0rP5V0xn*>=<2$D|M*2B z@pG2?Jd-}D>STa0Ei5eb3=H_c=_BmE_G8qs3aL~g`hQJ-tj0o^hT@{8rmp4zNE9yH z)ZA=1XIDmgNLpLdXXGv~^Ydv&ZW|gJYG?$#e=oOwb;~1SyOq+*Pv_LBQq$l4>B@#T%I@v$u_mN z!m%k%^s~a(W#LFiZ0XtKe@kjAnVR{tEQxIa78xnAY0W(0b*;ufI_lcRi$neWGAb(2 zTg0PgfgK|qbFW0SwYT?v`O;i|K*2abrn|HE^NoVzFt9-rOUu6z`^9-8a-W+n^RZs# zD1JunfL6H^bDqq`*~`n=*!ZuaZVZXfumYa}X`|r9i}M#QtkXYV1FUzrzYTp1TBe7m zr<%_?rOo>fAI59$UbeK%8oL)+&WpvyFkmEDczGFxiDRGw-M)RhEn3Ri!h*}sL(|Bp z(^&lw@B&+BaABBmWKWdsUg>AGZQ~a#VWT&HJV)_OL<>ll~pFF9B2FiW+U`NU? zT&6+z4-^=%E-vT1ygcG!#%L%F)9Uu-kN)%L&libyo|t+Kl3W$Rj7g_V^rt0FSKLF)Z==d;@<0?cA`l+juTvTE{|E zhl$X@Hyc9R+}fIT!b<{3+AdS;+qJ=gI`X8 z`;(D;fA)rIwT?mGA|k5jX;?=?A1*8`E^5u1#Q*wM_PsWc@9bN<&%CnKc-BKakG*;T zo*bhnBjX0d$L{l!aH%>Qv~>>l$B!QXH0+s#?jJJ0yL4H)VceG=U>ju%mnPtat_x4mrUeU#j>!3 z?8N6n-y@Y6auK?CLC>d8i@-6C967S|&ga~@0jOR8TOV#aI+6&+6XeFNq1V+e)6{_{ zc|b@3PW$b{?f!v*ut1&@o(#Q*P;&-Fz2r`H?-=>iI#V1$v4HcG3$nz`9%h3^fEPHC zB2lA(FJC+W(cHLk1Dy=NV{!3j+|=|mqGVxd$#x~Fg_yw^-8EENS_&nVUzPS;GA7?CfTKwt3;nLCIbE+N8K${i(Q^5O!0ra(Z+d zlfwxuS{#;CR5XNx$ad@2t*wugn*daJxVg3K^w=K%$T`hma_Lfxc>&Nxr62_m24Rcm zb~onR3gu);Y88noC`;)w_<#beuCCtMO}Tv%0no{_KBRYS?AOnq$Hm1_r!~KUsxm3D z(2@jF=^qn(j5}-L;R=-4EuQhEdI_?xjdJ8_1@(nZtI-h_ubU~^5 z>eRKrPn0vX$_>ixK%k?vKHU5LNdER$c$}7aZmH<(Y@Da+HE-f@i&!E(9g~tUk;R#Q zoEt~L7a$sLUbw(9hxN6(af8~47~dp0Favc`TBTs^5Imx&s91F#OXzS`6c!c^3=G`j zryyQ4Vj;3A;}+y^7Qmv@h^Y+@#Dzo=;LaZP)ic5^lt*qtFW*T7U|-pODFqqlNwkMCvK!i z&@Rh81Pubt8Z$GqyS^a&LOe*vs{j)X5Lcfh`LAzmyeca4ev=$<9A@I{*RSCbKR;1@ z`h`@SgeCzL91{2kc?%*fwP=G(A`{p!h%dso=Avj&V4MNJ#wI`sSqT6E9m5ra>VsBlG+BZ(Vh_ zo^RiNE-g9Q+5$&wk`#d7tER>wibCqb)VWVl}mf{v%HE^{p=S3XsH2fY_#mF-vjm zUw_t7jD}ujYHFwwNc^8VJDWzW zc%||c*+3E^wv;fGP!@|MJ<9ijzXiRlnWb^%IwSNUF+F!m zfV$4!ettG<*7(ZG%F^b5wJF^qiXtTZushMzM(f~wIT=WM<~-1-Lmk&UkcJeu=(ra^ zw>hD#ymMWqQ5vyS0bAKBFJ8j8Du}f1)i8(G^8lO2#l^+bQ;#N(oWjb=DtM`(t*x!6 z#{@<@Kv<9x&Yx$GJOZ5#N)6P^qm1rZiY_4@WbQokB)O8A(VYhv`n7AkV+)GjtJy_G zOaEfhV$f9Y?r-HVDd#Kyh?y6V$t?U!A(Az zpBjO%yA1wGh>$@)(&@fB!VQ-8NwHu$#qwX~m!?%LS*QGD{p$lGFsgs(s^ZXRx*ov@|t+UYb*b zToziRSrmr;5$GRI+TKB(-y3dfgcqnkLL5=zDI&M0eRvKEvZ?+|FmY?dA}J<@u9JQa zsW0zI9dh-J)x1`*NU_GS5`*VD0_O&;U}6F zXur3%B8~{im{=f8mwt1d zTmkhIaH3(Gk0`q%)tX7ZNYRT&3Qi;?x4T2qkJHgC!n&iQ17^}^hmME~DTl%&?Xp2A4ztKh5_cywd%h9 z{?d2vW;n&jz^2PZYB8&RlAOee3JVEotpMfSy`C$jth~6gqCON#_2Da^5-bJ-^t>eX z21EHN9i6a=*PU>hVSY_cPQp|J@RujOa6=bU4#z4#Kk}HagToKfKrJoJ5it4k=TAWW zJ7kDCB~UfM2-m8-N!A(zk34i7aWSzSDteOHXcBggr54VIf!?W;CxPAZsRzV;2X)9Z zI6pz#*@LFt+RyLA2PGZkmNVnrFOZp+`VuCXkEHp&HPMU$kRifoeK0XWgxySW6}u_s z8Fm1&oK5_yKAQXkhY2WX3F+xWNl3n8A`!T|2Y_?Reefs2x8}J6J&dGRLp6X{E)T;6 zbu(afq8{iSNgX=>@&fQ+FuAULbfs#{7WU^7BEP8UwLHrAcSoNAlrG-kn`h222BuJI zRv8yd-g0p0`mH2py{37t8GmtV*#UY%QgX7CoZNM<&rj6u?k$a!f@!}13{{`+VCoIb zf-h{CzWRD{w~#0DJTQR4`z1-OGy$fI3ky-J6Cm=EO)oAkE-xqSOwP}9qS4O37r$g@ zXGa~&efkt^cUBgbJJ6ng4vOnMy?5_kC80^eIq4^pM=LxMDwEUxmdu4(93IoycbAWn^~P-4T;_>fPj)EU@*K z7osPuxa&7`ivr{sgo0~})L)WkBf5x8^T?Q0PPZ5?okwf27Ii$6L*8_$#~2iQTw)O8 z5qB8KJctUcs~0D)V7R!}wzgVsvah`k0gfrKXv3Rh0`%hP)2B;w*ABE;d#k`o?PR=w z)>)0@3kZ@ZGB23|rW|}pbLr1!zB&l&37Nrlb3ps3KbwOHSaq5ZP1Vf|(ji48GtXy8 zRDh!zCZ(ttRSuL#xDjrG}6TN()yDP3&?XQw2?zpDicXqX6M9D|vT6tBk3BJycbDE4O`pl0C( zw5|+meJ|w9Q2*k++JY*oIT4D(7OcNTYLD+31+js(us|* zlH%td97kAKSQ@e*juT6fN6v*DJ0rM@bJoBnX|MlNE}Bh(#!dAcG|@NX77Wgm3YROK zg;7nhxY2zFII&}n(2DAU&ySS_T^_m^EnMahu&=D>p({~F*moreOr7l{&dsXV6jtKp_avD z07jcyX|>cUk-xjk9@@OT?v&?r^JWMNne{lxIh$#QtR<)xW2C8R1x#nD@Ds?{lUxA2 zjj$w~!Q>}h`ucUkxWj4mDP2v?U;u9-cbm>k)#rQ}7`T@gY;HX~627C>`Cd zZ>`dDe;7Gu)kVj}ft3EPq=c4^?o)ev@Y&AINkP07^E{5P zC6a@OzhM<3ASZVo%wn-O3up|PpZPtneX3n@O#?q8{Po

13#UCsSNO5Cdf-HsFT617E-NKSfT3+J)?I9?5htm%w1Leem~F1w};z{rxW?*ux1V$co${DmGSt^!MD{ zsj}#lX3Fg|3sF%>PEJnW@AR7f!^FVW?lTHn5}bgm0oLx^si;CZ3=Jmxwd3=EtL zAP1U?ZbJiISe~72h7rdCLnZ}2is&^8Di<&s(A0YSzfrE`P&ky6T=_=V`j(amaI^*Q zm6S*U*W}~l+qz^A%mflGAUg`8I;ac~WBnQi{7Xv>pvsDgi*Ky2FW4e9{gX7XRZ5UC z(6PRC>y#Z9NgE!m1z7(6efGO1F*r2?916CA^b2TcXy74oxp;ZE*4Aj5nDU3@(M^#w zTepCbRH&3(arX3l&W)!v%zyR_0CZke)ngK7kf!N#{&V01l?5S)38L*RbfAN*NhoD&sIpET0K$-zl3gl$# zDpVvc>L_JjQ0ButtB=mJpV(DZt9yG*1M2X`d<*BPsa?mY%pH=*0LQ9ZR*Vye0D_F+{p%^#$6zZwN1|7VN%*MbAGg27JIs})- zotV5FfdsZK{4CeiCPnb@JlRHo+hZg%TTC5bpR8j$f7$E zD=L)1R)?WQs=*G3BfK!nH-Oy$E|L^cU$6P^3v)!n>VfnCpR`9Nq$y^JLm{;`UGrzP zK-$mAnIz?hh&6D+W1E_qbl_7!jAvAEPlo3^mP8FQS4zYzMINM~5}}<06rbRyr=x?G z*zxJpPBW#Z7!3{uppcxL{D4A>pQIsw`NBS3hA_lN=~C(QSXx>N2t0*=5h=pM!sKZM zk`VkpEh8g_yc|-RiC}4fjcO(-Nx%Ui=fFB0IRs11gF^w>$i4D}Xjyp|8lQDdHdQ|s z4mmIHwa)01((p<^cVYcquanx+Z;%E5dEz5U)PVB9`mxZwa^*3ExPTFZo#o`T!aqX` zj20a3N|))D2KC-Bf%71WK{LL@$ZYYPgTkTb%a`zqHTpSvSbs_e6w2b#CHTPT-auNw zu)`0iO~;YZI}lxf=)r4`pM$r*zB4>^ik`d(5--(-jSCARvQ*+%c3uVA=f8Qg@=)#; zj3JyMAV}cUV!tMtD%c45J|x9pqKx?9-&j-!=0nH%ix;6?fZ(s#r8G&p{e}7|Pv!q_|QKY_$K^-grXaLZJdifTEOidDTN}A!TmTjJ$3;YRbab-!A&bz)fwpAc>QNUHTdE62v`ssWSG8*i z?7^x7%4YT@;p_}@3O7ST+H*uet3VLZ36L41^kDFWmf7SG7d3GSY-BStghRRa8bALs z}(6B;6C_n`4>pR=z8a5Aj*8%L>V2Dxp>}|eqe#G3ny)+UU z47{CgY;9vhTL7&5o+sc+T6uadkN%jQrJ4?Ay1PB*g<(i-)G?LenSyxa<+3}?d8WId zW`RU3MV~r)p4+GZPBFlCgyZ*`!_kj!1i^P#EG+|pKftjO$(&i{f+?x7586b&@I_}v zBLzjpjG?Snh`+ph{~pfnbtaixOO3*39vhrI*^to02Wnh9q7Nhrm7yU6ft!FBq#WuJ zvK~B$_KwrwWn~Q|M1O^zg~nPqYVRDhLSEx=`#X53jV!R$%(cLdM385I5`p|0 zfs0>$Q~L*Ns-&Rc#n<}mRJy#=c$_w!bzyio75^PuTfyhEP_n)CWRN!i+gYg!<_@jP zVLw<{;O2yoql;r>W5d5Ov=7$?sy~mCKiE+JD-(d{$Y5^z1N9Hu3vg1W;bQW@5{;UJ z^$y6Bf}CNq+;d5mp$h_3^-@bi|6=8f8ETzG@JYF3;qSFhcz=~im zevVcKRAHoLaC$biER(17p$?TEOj6Eer6cB?nGQHLp6r?7OgQHd2nhcy#X^aH&}oc? zX@Wk%+$;RCS`BO}JZsJoQ!aAMj~(!;lDbMG+0fE|55NDts;xZ@4+=V$UH4Si-`iV~o7>Rcz5F@wj~fVvKsxn;`RjDi#W}|^ z_8`FPN9kukEfm?vm@B7M33)SX(Gywt(HrHl&^+8J^@XA5`uOaEM3h@J3#5@CE z&N?|WGyVngE0f#Q5qvL;7$r2@g!S(Iqq>pMRHTR1dcJ=3T^u;JR84Bgf_%Nt$f<)m zA#%%=vj&26lyg)jas%+f!UqsQ1OKWuSZQfBfWd-3IYR&4*<54$@1K>wcShbe12Z=K z04RW>DmM0%zrV`cUFc2el9svD^z`&PzJgOpw6-%*<&gd|VLgyjP-rzLq`(UUz%)l< zr-3|Uz@9&Q`Lf@^&KeNWnPox*2sX8HHuXsa?z}$>Epfxc!^GSqrgI@liGLv24*6V@ zcu)d5Iqy-lmb||;1SYwNh)DJvpnPisokMPgJAfjg^sN#Yc~zn;TB@>Lg;1%ZR%oFd zz~H7&RD38LG>$g>;;fkrl_C&u$B*xK`(a^p!0>M*ulxzxYq5fLYj!LfCElUrm!)gAJNdFaO3ILtOQ zIWUxZV&RyB4aLc&#KOWr-3tt$Auik4*_kjv9>2M|TKw+aW8Rc{AT6*WV2g_H(2oHR z3zy--V6xvkI%G?MP=Ea5YbX}#z@5oRn1_%)`?0v#;L-g=mHt_fgXR;WICz^NN0_JB zSEZz!g@pJ^p2!;5kK4C@fq$VPgyR8}Vy~VMAF^e`hD@E{r{YlapUV7>y3yA&HrDlT z%Rbvc$rs7AR6$sSRPG;e4#cR26_`7+)b3s~F$ogn%{h{cky?S-4*}92fd{RDxUJ6; zDCD`Q5tx43DCnXg{Mk$t;tjmt)_Q}Jz~fc(^d$=pgkTC_#i;P8!L~dpvQvW2wn5J| zP7Jftl_PohA+H|d$4D=Qp-xIk3EAmdbaHUeRMe0AUhSt4q0f?7bOfxhw<#qD&LI0J%|bWwzSxX1uDD{UDcJ>4(6r$|wMyIdecln4Q6F|yzb7GIwx;#-(r zg2}=k72f8X{8Zh0mF$=rTYYmg6|HQ_Iml6hywK6VL-HBZAu-eayiaL9G2!evP&dy0BezR{CV&j_y9S4Hd0N8*G)eA#x z2@VC2vElnVlatLIO#@$iQzF6}fy}{XX$X}&edT}D3*OM`1bJ-+E~*)PL9+d9l1uaA z2e_EYdqZEofMYjS?qcKaeokAvvA;i6oe`frTIG}4EsN%p2IvM612BHztw=X<#)Vl& zRsVu!3cL=~XAs53hT+c7*WqCx45j;jL1i%n%%4OJFxPXa&>ZK06D08^2;;*&Adoui z8+GB}P$Y%Ifqq#~06|~o#k$vwAeZ^M7E2!0!X|x80YU>XoGGiOM$OXlyNq2o#6)^@ zPltGaH*&T#Gcq((0=4YpM;~CC6SsQN+rW@F9i5!)w6(C{nKi9%QkEc#J6Hja)6kcO zfqV-Va^8uh6Q!Vy3d3H5u$%e#F?iY|G!H0sLArz`AdtMw_7`_q`{1U@#N;F%6PIKQ zNK4ZWo`jF*6<`^ccQMh?InXZG)Pg9BTaQ7e#DB6O9AMy9@w17|Yueg~t$L85CrfZB z=!vjG4XU)!N2R1(+Ph_mmxG-+I5^TkBT7$Khae_Q^#wC{Js8-7gIkaTfGIRSJ`SBf zCkMYpecIRY#%t%#%7@=8O{PCjoLuIK z2qaOJN;l-rg>UZOPb*KKj%8naE`kYaH9Ak*NR4KgtH<9QHrbk{P}ZhE6ttY?2ZO;$-xTe$5{;zLOmY)ai4M0 zE?2Lhx?lW>%hA=bFGRjGM1zVT@#ihpMEDWhDhmxo^i$+@MC~oqCD7r6@*!FHMA_q{ zzCO)M)N<(9|2sK1$NkA&vW&)hRUmQ zn!s#j_m!4>nm_0$gumk)$^4e!I9j?8n;UtD1&sMU@38Pv&j`NBt%X7m&E|VVhP43CujPP?GALT#-&RS!EDP5 zwp?Wfmz^83GMz$laMvggEouHl@geSxhgb%u48hEFzf&bESRQ{#>5?Ki4bb7BS3$L- zc%ZF$@GQA5!b~bu8us`inoxF)wCv&#E|8GWQ@#eqo{33PR#p}yej(NccLyFk@Y!Cz z>KH>ZFYb&1W`&DJ`qG@hWPm7ga75geQ&il+wn9P&5>*F>hmw{xOY`$IYv9gpt*iN5f?+klw6!AIR8AKu&1OEZV$w>F2#j#v`s?UabvQhny*Xb$(Z_Uj4D&d zu5T1cd(qj|)z#T~$9uUsRm3^T+77!z#YdyKiZ4|P{G*bvQ^oK2pue_5KT6BelCL4B zrl#icvN#pG~~1ZJbhiC)1LF}S@;!HEigd9oJCAOY0>M(F1Y&& zz_FyHl&6-eC z2sb1y5C29Ajnem0QE$l=Fp*?d;?GN1hdSy=6jf!tK6hCfN1j|#MB?D^c$H7+J;unZ zJZ?V17vZK#@kN{)`?6sa1*9%Xyc+a%eDiR|HCgR(b1Xm=HMrAi6TNI-zfQ0a$1@vTpwacfXR2jzXIGM z06?Cn=N1^5uyFvwU~J>G!E21V2eJ%|X+V!bafI!Fs{7z>U|; zcOdSqEG*P%eyFVER@>YA{S$o6CEq0}S2E-4M=10HJGk+~q5W~+M=J#>g~(q9^+e4c z!`qW%>1kn4U zL{W;_Z~+l8y++LL^`?Cq!{Jsjl$Z(rE&~!-my=XIkoLQE1me%Y!qaEYJlB^9)Q>)!@AnMlvf#7)s?LbCa+MMs;ZKbvZAbwL}P2b8tW$Zwp5C#n)GYDPj(No7|Vb*CtYL?6a`Yo-I0>8Uf$}T+g1; z^;24oj!XJ}&#fN&y}RqW$gd0ee&F{m{%mUR(Og_Upb*Jp@cc>COT8puM`7(!?@7-z z4vp_|-Xsh>=!Aep;35K4A5>+?SeSZ2d3#64QyI|mc|`FjYlcy`)CllSqfE__y`bA& zFf)7fhj%BDywd|jIS5*wz}CQF#o=%uT0kt7XbNoybh+$Xc5b=l<>rQlKLA^hK3Q|8 z*7tWI>`N59r~n9V5D|L$@}&(*=;%>e%U72v;W41~an_%S3U}LGzs|$m>>W*H>~n@z z^;ADGW-WkD%RR|3@LeCF{q9E~#I#CMCq`Cx)@+t&p&Z~Q2~pSv#2>4E0H+61(GQM^ zifXkvI5~lb9oqp$to#k=@1PsdkS{SpLR=9m5FNHra+N3*EISNwp;xUP6d)YK$7Yyx zX=0s&I6Aa7n^b~iaW8l5pIH%!km7Ca>w{}$ajHBRFi&6xqtWoHg!Zw(*Ptrji&Ynr zl7T}G^2%mR>`@xizpxolIayb+(21bqfP0;jolS}jl=GJCXkd*_pZ>A7f1R9kqx4kV zeC`|$Wnp2FL|sx95SDQq@(z5jWQ$9)dnlqp!o!<^$U7G`d13%112Tj({j(sw;-Oz_ z`a(CT`<&Nj48J~2=W(3G4h~>#;a=n_ebhFS*3*cHmeyk^3g-p?oz#_>1;haD;0@#vetmsk zRaIrU{OH=|dWlj!iG3td7Kb{GyuDr_t3jXLW^Ok8VRn|3M?X5*X<}#y!aUt=rH&9l zl6cRkwe#I>snh@r4To*jwT3TO!x=#fo*$-wYuy3MqaT5j>FVf=!svmM2|T_psaxFP=ZYVQ-(2oQ!X|)kmLSRKyS?uyaEQ-;S#d zk&Tk_?1Yku3YP7u9z3OGU=Rfd3Ul$`E%joZ#CgcTWh%Px!;{L&zAGqbgFqx)AySu> zLbP!*r#9`7DM&fN$SHFi0T}xM(lr@bSx(oldmQYzK(DTEFMU=KYNZSdYr^A8^Yd+8 zZ!h!6JL(996nIkQ&Y`6Jl?OCjR?~0>_H42LL;I|yxN^%fL9<|3g-s;HoP3% zT&3K2cB|!7187tW>y%9OX6!&RK&*vC8t9?5fcXLyt*J9)4F-Hx5;@FQ=DHpM7{b*6 zu>?&f!fTHYdjlN<*4J>H7Y8hoO zoZr~*sSmfNkG$lpd*p@3xEd{-=w7ICosGG7Zy<*Kc=iQr73OWLMhRn_VsB1hJH*5W zC2J2TY{2g1?o3|p8f5W{xpGI4J`1U-MgH1JbC2YF`&&EHm0CiSgai##UAtp znO_6a(3uRF?;pSyk2tBP*867p3 zgyx+)5)ZGQJ9iGOOjFY}hAf~E$qO^iXkNyH^cXtH42jkhkG^{L@49Rm!2nBr+ zxcp{DS4z-9j2X90-^?!CfrqGe#vse)^`$}$LyHl_Xmn-L?9LyD(Yi}0DOQ^rI_=ohrG z`T6ChLTV|)Z6r=$d&FiX{sqYLytS@>2CX=$x*@C^db8cDe4#!I(i3k|+=;rwE5 z@|^kM8mI-E&R}x5A!(uNNF?1D&qapTuSs3)d~drfH*7#Cg%luJ-d0bJcE_xT;nG>T zt$JO>DhMg-3-a`UYfjp04kNz4tD+PX6zIn{k|RK)0562nrb!pXX{eW1m)_F_`;7a^ ziNdV}Ux9|Z=51BXN0_UUlfA&w^YYTZ0LKW=3SV3xc0&Jr18v1>GgbXFx`1joO1Z`~ z0{cL_>)p+IwqCm^M3;t5Pu>uZDvzSk(|UV(?DuxKinIod-!oyJ2f17D5p?XzIsi!! zvok+?HYF`=%+z`QiMcriaWR|^hOl^NEAGV@@w-xy#g zd>cuGcM|*u*PyGhlWzWt5}2)v_pTl(q5M%P(#vSdWvF7EFY={EIL*1Q)XdZrFCfrA zIw}?{-lV5xhZUq8o`;B9TF3O8Bx<;n3sD8y(L!3@Bx+cifbUcU8V8N%&k74ql0J81 zo$~EX!S_&oZPDRwUbY9Y5FrBCCXdW`CRjJUU{tG?$_9_)qb8~}jKxdGI0}FD;H8}$G9-iAtqEW6w zDdv}m4CWU9zCH+VVOK6lGX?UgtlXnsss@=eiX85h^-#bRgM$Uo7t)x3Fd-UHS`Te= zYvntf`b2RD7;m62L;9V^h@U_}PjHueZN~qx&qF^!y~46 zwcZ9WK|{k7FeX7%fA9l5#$fK->Ehhx{iEF0;~LmBh=v%1Lbd>`$Kj0%)=?W1XdFu+ zxRtZG^;F0U zNG8QM@dP5AB*0k2?JN2D`5+IckKN7$>EqtLutsLaP{<#Z@GJ@4^7NAnaKSV&Gtr#q znT6tqK@YJNmn70R)c{rT@`A5gsBCgcM!0?YqwMy0UmWsXt#4Q(p{@PYFqS{?VbnZd zkae0W)h4xN1v>ultE(WLLvxdolCoEsqu2=9CFP?Z!#CVa3`>|JDNZVyQ%6~?E4nHY zI5-kO7fVcJ3S^fE`e=v{GKRDUrQ>Dbf29Tp&mk~G{{AoyyaZ7yxL(|#E`P=Gt(dR| zv!~Zkk^QMX?Tl3MPi^}`}dyf@vU$rjfNnGugYaG8j@Ss$z$Nfq@h{-}eUov`^6 z47EV6Rwvcv6mwr1MQ6fyNP=v^AiDi6!qSK{ro}Rxbl!t#Q)R{2(UT+3th~K3eo@d@ zb=c<^`|T9C`IEHENyGo0j&#ft`Yuy>ixqtdjO&NbN*3+Yz(k%EJ1N zV-?RA=DeHh@3bz@5*6%AAGOxQWiT8~W#XeTg9J-Yj>xf3U1uhXHxx3qFC~EV2MVlB zGXL>%@cYtAy}#2bLXRrVb`V6J3MnhwUV4+a%>56q_8YW|4+HSoNJ7xnXujez& z1qi@{Ltk9>k;K^N=B&A!Yqm+|va&LSdr8`|sHk;p(~VC&yO}E|8|qfCEBCKnaBezx z@nU+Xq~Vmw8B)s(GY9GjWw+!T7WB6=@Fw=aCFeavZO+(A0#x+@HF-#6bZ&8C#9bErtMVP!Jggz6N6s5Dr|n?7kD(bQA8m z>k&isj9i_jgH$!-;7*7HNyN>V-K0j`uM=M|YP_sTY>7$j)c;|C6{Hzf$qTMQXc?#h z3>SRUVSpWvv=wJpQ&R(N1`ejR%8B;D(a|_`bLC4Pd;5ZLzsPz)FY7vRR>8psrvyk7 zpv?$l9%c6haGZ*G+bFq@+~RrP<^K06S)P7DWZvW}pc)$QkIIC0c610hB%yIASg4eb zndmSAunh=V%ovrPLeTe3kNO3V`&`<;8!FERUn(;&P|W#9%raP`R8>XgMy(2-xaDI@ z4^bUSIk_*;!C@c(>Hri8?Xl{&oS$k#oheQhX8FoMY$DnKv4w5##2LaX8iazpZ+l0W z5+9fGjGQJLzRU-t7u(km1(@ZOlmJL{{6dj=>5VRUevtLR)7?*>FY`gifiC%fEM0dz z)&2X<-g_UEQIaU*nAt0dY)RSK4I_n;EkaZZk*s7@W=Zyp5ZMiqG8-})Lci-gzwbZK z>v^6(oO3>(_xm2#eO=eBmqN{!{$XO7(v0=m>dJAYWa?AA2&bgjPUPj~HE@##B7L7< zcvn&V*-jM`FNsGVg*5+^m z|3+18ZD@G)Lm~aN+vID`Pt9R!?h#&kCedsjC^FGpza1Uzd^8AMN+=i}Sy@>(Pf(ch zFSP9xt6AKGOpn^bAa!MBCB0&)V6SGNL_{#<>DK)nE#YayJ=7eQ?dnd7N2NzVrr3Fc zKss^ZM8^XdOS$bJfHU))(^g=m(~ByM?Ck6ys{}*v4`v~FcvJmPP?#f&q^6|M ze%vQACyp=QdHnl5bdAx(vA1t=OP%jA($W3d`up8X?kZFUW)h;Jkww|fz@FipaH8C~ z9kNTzjSommhtZF7IRF_zF>1sUf+04h;%O?5*T(W_Fsks#0AlOLR~hr!<@9Ml+eajs zxFxcMVg+y?kgLG_TTzOOFcV^`oTPmgg-R9F#m=_2WTFvG$plWH^L_O8495@rSG&D` zPUD%}^EFB+iY4U(=mQ&ueNxPyIIyb-?_Cd(x@}%ycyvX8?k9 zb=_qtgfhm7_lNrK-3R;G$uL|&`46H9$*twVz00-meGw<0A+HSEqG1*Yi@x;t>(j#i z)>aD04yv~GZl7)7{Ub_~@u;e56-fucZzzh{L*LGkb5d?WKS%jp2J{DMr7b+A7CC^$ zVUws6Fye7G_}vXb2AegKB5+dmq(#vsuBuBv0h+O%F9=7FaC{kMTJiL&A;E*6oC^s$ zERWS(6ui;p^bBH;pGhkUFOe&VyZ!yFji>yU@y7r{JT5NQz5rYfl_BI|Y@D1`aK!^M zCA3X6@}Nz$jV43G`oiAAZ5Nw0G0h3x91Sh+{8M(&iMP#dL&W!A>yE9krUc0yf+%!m zP5k`W`81q^K*amm(fDukI}V=Hc33a{N^RWG!-s$Zqe4#AYy>#w^mqkTtBx~bB-EyV z;EbblOsvM8$>-gbJC5A_Gl#y7Xpllo^7HdwQCaDl9(*h9fKmx@20mh7V&N2rjXUmx zey;J)3_mUX4Cfn}Bt?3c@s@i#tqCYb%FH3GI7BFD?IRi#5VKFOHcQZvy$&;q%nwh# z1W3r>x_gibMZ{im)m)_(iQE@ytm54-UO@08|IydNVh~w_wVmw(n>tFxFJHeh1!yuN zVs`8l;x?tv|59q3KTq3=LA9P{%MxO7>>@~He>cJ3`3$1|a@X$P@g7&R`<}mbym6D7 zF;4R@$ngNtqAnF~lhFEo<(;D>q{85MRs3au_S7)r+>5 zlozgau0|^}*wpQ0k_(sLH|0)1rXoX?C_*|9xF$w^`*&fIu1D09j^Uw8wkeOWWl7)xv3T^$}jjUhra!#1+6qqMQr!3U7sO>p%$W+djNegpU665 zT-l1@Z;p5E%L1``#7yH?+%qzbi4`V|T;sK1a+lG@>j8>q>M3>I-^)@GV-`}6fpNCla z#GSsJOq7%w8yLH+*jKOWc0{2rO(-W%W5I*gVTUOwr`+blYW%ja4w4^nS9f>u^L_(QBexD zdm5o|mAF!DD(Aei>f!+FfMRylpLB;cDL_65xpc|;p=|)JKST(hIz5C{-2gG761Q(B zv%`~R(^fBt=XB_Iv!p5sQ_1G+ZQ9N}z;;=XYK3Kp@d0Y|NX$3IuADKTK1ZJw?khuY zSp;(~>5&7?&CHdLA3XTgaG#7}2Q_uqfpc@o>FIozX!qtmc;J4X-|;n2Y~{a>FQe(c z#qpjB9Lp_hC`>y#%)5GwQ)zWj_ z?{^c31GluoKv-}(Y;z1Wg$?1Emj819VB*jE2)H*2ZR?=;(QcbDf3oX_Djb!5Ejlpd z`!>_7vH$~R+H~?9Nz`lo3~&84L>EN!19~-zYFN40c*k(U-+lwo5W;~?IKK{ z=!7|bZVNC4>p)H}J}z#Ll$4$k`x%XVGHc??K@JZEPWq`zc)itUg}Hj)mZx|rkSmuo z8V}EqE%=bo)=FzDD?fv3W{cI|>Cl;|C>juhGC>`Fab5IF{c+sH!C_&fDP!Z)15wnA zRL}V@agzCMe@gL+WPFJzEbtte8AbV>jEp}z2GqzaB>p`Wipr4@>rcBr-iX#^9AU2H&m*#p z{8;8u3mf>{J$w$YP6O^`(CJ_*hBrs@?!YlU>=#6y(F~gQ8_@Z_xkv%JE@KlABR+x`~K+PnHB9QE9sWt8!qDdY-qsBdt*_LH#u*CbB0UEYo-j_|(s@tcy>X6OG9Kx7BHC)S?g81X%;k+h z`cAdv`}UJ@RKjoY4O(j~y)H)4G19CExy15aR}=YFyP!>xHG|>Q!cN1WAm!l#Srpc4 zLCZT09Z%TZ|5<8`$1$AIKw>TiE;;IIb48QAKKml@q1~k%qX3F4wHCU&ti5C=&t6{1 zKu)RSl)@;s+^~1N|6knD9)aqM59{Tlik&0D(b;TS>h|FnSt5AWpQ+k5A|)ac5u$kM zw@>NzdMb{Bg?|Oc9;#InFa@Js_0$<*7ZiM7>24D8>$-MlK@NT-MuM;d=D;yy3zf-=PI@Wurg&xLioV z_U?t8du9Ifmxua?5~Xzf{3_C@o5eiBJU@u*QQT=G`q=oT_&IWP6gjGAc@*y&S7Zc}XVVxziv?PGGsGoPNeNJsN%hR9ImZoj$l5ZNgEjvWp~Op~Vwtt#Y3-xMb= ztsp!|;XXSxHI!8vSWc47eUZe}RXn+=l|9NZpD-dCf0A&_vu{ob%ke3sk@jW~HNni@B3x;(HcltI? zIR93lzOAt_#RA!72j@pEvu_|0{!}{f2(^n6#m;;BTgxx^hpv@4kfaD-TtValDTqa5 zBiT;VJzJyFGi8M&0f0Sc2cI0p>4?tz8iUT~q}R8d*1P%o9~>x3x8HhG>7EpPVEo{Y zko>I77QW*Xiv8(Qp%%qLbrej}ObG`PS`vv&lqv~=u^jYmK1-ylBeBUPW~UnuKR|6ep9A7uv z-{H&sc<79f=DB(HG!{1<-ZI)%ri4CNI=-#Hc;ORSk>h<*&oU5s4*K47e1sJ3T^Oa7w-p-%>!?YaBf9-pJTovT0!y)!(3M%FML$?cG$0vN362d9-x^PD9k=rNEuE1;J=oI` zWqW&GnIPGBb(O9nE&HIal4FZU`Mr%#yQ|Ds-e+G3yMG>!n=#B*Z=_WTU|($HbV5S00rV z^&nvgMLngWz*|7v0r1`Hv{A#~9;SJ;JWavM&@*?`6wOO8? z$`KYW+B+5*eEKRefgLGN@5B^*u09_e!|l@K8afab>j2u&krt1kiF2YmRl0qW22Pq< zn-qOGGQ>q+N})hLfA^8r4o${>7k&Ty5OA;nTDFNur*Qn3wq#{zq{-R6;wZUl#runD zkXHIp1bcE5i~4U+Ew*`UUpnLwZ@IDdICK+dKKyMq`4ByoZM7HIb#aAgs)5}t$FzM~ zMjAIh?+1J{L^jyeVZOsk?F5xz9Jix_`H=U(RP;|}>v}O2qM&)Np|a1?p)qxBr-EY# zq&g^N6<4CfkABo2!e7Y&b-|GXvw^-WgcBG z5JuKvH4b4lrzI*A6w^)wAZvs!!b~YSo&>T?0#LH zKwj%(!e4uDhXPG)X-$uO+q$ltlZWq6SN}=Kfp*R*>V7m|AM25yu@iM2QXw}zvw!f7 z9J}3Y``$(CpRiFIMGos%`LZN~$HBc%f+cTiWvZE*nl23-+%Y93&M~gZwzul3CljA6 zllRWmlSlG-t{4)Ntl2t*RcmOf!oA9l9}|p;Ieqdz1vTS`{VIlvE*Fce zP&~{@V^-f(+OK2kaNBtWFX_0sc|@TAV$;L>#WRMMS0n~HSp)BAH`Zk5eN(pD9e9Vb zUXx>;kbb2DJPzlJ zPhdgYuhTBWRH&{d3|G3lj~?xHBJ&MgaCz2aGOO$zxW-LE`;Fv>cWV&g?B{_3Exje^ zX;LDC0{r}1BKjA{GBzF#*$W;%VYb4}clbtfa&}*AnjB?wHz{%dKDlu^-;+d8bfI|@ z_QPFLUr)>^O_Nb;qpk4aYQ~9=+t}e?n9wRzC(7B%dg@J892@1Xxv_H5_`G#mu2v@D zJu4hU>Bg?&JYtH@7lYL<-Zqbx*jEP4N?UNelXaG@kUnWzqvF4%eX*HSyCp0_SyNU0 z&nJ!!v8pI12KUxgOrI1JXwMXwU(Ag%O(MHy$bMMfBAYa0YI`MHK&H3*gWI`+xG{jc zBQh0viq#8M5d*jH+6)q`+=3`uAhgYIeVUvlX-*hoo5po!jl=V z=R9llpYVtpC==0au?95NvhAvD^m@n`H|L~yeRdaK~4bKp-Qn#Vupc@-!Xvr}7HwT7Cfk`A0? z8)=!y_nvATA5dbH6zz=&b#-H&x*ZZt5n^N%>BvEBBKk1;M>*674`&XrvI>9PA{j<5 zh9wKF#B)nIFYQtniqR~mjdANqoBH^%;?%fohp#>#^D+j22~*o&YI8=Ml8cbDukspR zUIqnD4#8{SS&KlCCZyy>Iq)6lJzd%VmYf_@o$Y@8Ua z3Ubno;v#0u6|<)<|9%fCV1~uwu?rk?;srS~G$r<4lk3hNGpTnn{P_C!IDFl&(Esvf zeE9LZiP>izxrbNkA6HwbCl%W_wTr(pul9QD{gpcP;F~a2-lx97ij>Y+zwN1MmqvlQE}lsc52yrBAM z-I_t(efzNcoF&Lv1>J*W;#W4cmA!mbV1AHLYfR%bv;V+Qb8~o3m|Yk1=cV0&Fb#dY z_J}h+{GOXC=YMHbnkg+fw@UvGHMth`34NKCnk#R-G1Qk3P<7{8J9@P>rRR)j4%F#jk8Qc>gEwuZaT-{#>_gUuY-jO+^im>e-=T zQ+=6$t;}CS_DEN-@1 z_uSK%BrhOyD763W(N}Ww_?x4ezAk6|c&okYu665KV;&@!9sVzy@Wt06JRsGDe6w)R zmFnB3-nrLD-&9@-B#A>my3SvYnm<*&EHm@0L?`U*taKbsb4Nk_4O&@;>_=zqvcB)f}h-?QzYNCkG?N$?A8C@hbc6yrQUc z9(0l6L@J1JNUdbZW3>b8ohm)>eZP^F!TQ);%j!=`8 zy`gp6)`^)`oX=u4C#sBbQ2OkIYK3LlOLNJHI!G~O#7lUeD#vUWvFq5Jxh!(n${uxv zBITcxlfyp<^TErt3(Drx#fuJ1qTkE7&fXtcsK3P>B)W5kt1-TP_vLoVMa9K5(;W%b zKJRV0T{;V&DSDsY#h@wAyNXyfUP@CVM5IHZg}(c2zNb+41^C@PFbjs)+?L4H!ufps z7?Gty(I+1uJcl8bW)Ap(eGU;vsB3_%ejh8(Gmo!}V;?&0flWY+*Ur}N3lkLXvp7nW zrjMj%0trDk(pLh6SpjLsrW)UAG_8*sESfUWGwfP~=Nu=f)FwkENurcwV4;9RdFJht zuTWYS?Uo%!-w92TttOp==YOEK?-_%+w;80FKY#y{)@#&o z!(0G^BTR_;j$FLrICzKJC{8|uIv_~{_+Z9H_fdzUQ&3~PARBk%r}2BoDkeXM$IRrw_<2eS#N2RaLQi zIk8RL(#z#N@OnDEHX`ka2~ZT@F0d`)+I#tW!?sU04o&1AUrDE3oT~mQQG9j1K5^l_ z<}HB!hlywJzVXMfj7?O3=kDE#4h&=aP+lQ@RrUdz*tIJyCT8N71W_5FhN#L%UY`Gj zD3rp0X08989^WBTSP}f`wTdx?9l7E&O!Z`#4o#h!x19JmBSMg>brKezd1sFj3SL1P3aZaQ^(0w3s{nl9;CiG@dNFPoialuD>3Z-ygxsSSE^@ z!ma|XvdaWV0<)kB5l|DKvm{GST~bWIofwqoPrOHu80kFx@}@eT_O;5ry~5kl6i(j} z7?5J_T*KkDer$jGZfg1eKUE<0FfBM4Tds=guT2JY!(+3eYlv-=m;Ztmd9fYO7sG6E z@fRw`XJE%cnjRm|I43Taz?)URV#u0krsm?j(?2l zvGf}^Dqs}tU(B^+dy(fav@(EIPxWy~GIXw$IkjTAZ+v!y=f8tOW(86#>!4#0$;{pp z>GEmcucbmcCQwlo-Dz*#0x3KxHdgw9ElnCVuB>E{vQC*K(BJJ!K8}HbH2|sBJ;OY) zs^cDxnQFFc5Vk$dPECcME$chD04J0;Rh~D&5$+589`rCo0|EYZrjwu}q2S0}TYLL6 zcu@?IUa&u#+dc9pB~gf%^tS+ffR`PxFp`vkN9#sVBnR#v=Be$5r2!mA!8Y&0NGHfW((x{$9!MXI1fl`% zg)ld$Wxv+es{6ck5UUVwZ_6>sYf)%xYnP4_jnbrZYHG$Zg>7zQqb6L|Kh+a8q#f?!Y?4T%1An>sB9v@@f~ zfQk5D(*;#kL%0>t`KdT`9_pL}Hakp0K`17Xo~qZj$S>vl0-lk|I3qVhIZOHu9@rKby}#(}gDKwu;E6qye>Na)aqS}AY1?g^S9gPvp-+j-WAMInt)t7J+$k(>R-d%JFRxzn@IQz%s#v(E>{8Tz_~ z)V~meE4ns%`naZ9`@h>Q+HGCPaTXpGv2R)O1m#Y>0TZMj-4G;Kh^%{BBN^QGpwJ*w{ zl!Bu>6fGs)6|IMb{ATZOOC2xtuh2)44ZS=_VF0b(!Vx_SWg*7>Y|%WNoYLp-sRUfT z4#xXNOEd#y-B8P{gHG{%ve88M#muN`xKTG8Te|9@ejDB{+xA#gKY?`bNdW<9(3gQc z8o}3Tzip>Mf;e|nRs5TRhDIakz2E#;ZE=#RV}^uvVr*s)(}ljnI4$D$t-!k;t(opr zg#%x$xc19S6bG8MuSjGKf0%#13~o8)_WSN{EDIV``WLfD4XxNM?+%NlDVBy0nQ03i zlcFtf8Y^?cwIe-k=#YLc3^P1E-PfOecCbr^iIIHl2Ly07?ibi6I1sbpu_7oS&_=XD z-3T-gZ$2pK9`m$Vo;&oDq=8CaUhlZJS`(2`P$k8&iFE6sHTn7NJ?8QQnPyZCKYP;U zFiwK#9!ChenOC2q2elanwhdzE+sxdY$K6SzbzdDJ+0d;^GEDSe$VJ7bzu*A<``xMW zb8(d9z%Y{@6-;tR-o5()Oe;u%3ijrzj!j|t`8)mfxfX_65P?A0pY6?fibGjC0m{th zlt{-Qe0|;%yw3RYS@>W32Ws&6mA|V+nk}PbM6_sayo|L*xT1E?{C0hG&oU{bsRGHN z6*XHguH!h~(A~T5kH6=?`wtd`5OC7d(%RGo;s&mwH)~g<|ZmCE5Ntg4Q z2EPj@2kOk5aNcOuhHH76Tk{QK4=JEdA@j{J=A?JMdgV}YlFsl@^69>dERHF4paFp` z2p1ktN|A?H9OU})c2IEek#yO16^B4q4%MLLHV8@n$hgzq-le7%GS{E8>f^6}T0Fi- zsVA)T2zVatc3|kvh^j{P3n0kv+_{tWZJcMB>O#~`*&xuc{U?$L9i7-4u7IYNJA{ZRUd9BTi$d6`vfN(!%kWEA-eYsu;4`P34M@V zwbwAIG72|pCLbCl_VYk?Bg5nu)UphO%CY?_*Y^xh&DU^1DV#{F!WDxv@-)khn>Udk z!3GKs5K=mTwPHasITQW)Xbb2W807H7q61H%{6tqflkl{;>=J4@=&9E#ZeqY-JFECL zY6k}*2FdtV@l0kWep=KUMtWqp&>ci}_ok+&=n}!*{>Tdxq@t|lU1~7tXpW#lQBimo z?l6!D*NHxs=}&-dLZIy0ghVjUqg5Oq{FvHOU#wF=9)glwFty{zvxmN@TM?QnEMj+n z{{I_iw!3+X#KB!S&9~6byWq;}x4lqZy6Q-lYWla+eT6%!F2VL7cys|h%8^VZVV0Bh zy$3^#DQGmx@4g%C7h!c5O$Not@ucGH=A7nL-oLbU$gv3yYi)aBj9bFu12)jRuCBP| z@`?ra$4kuFKhS-`(_omBT8SlARC|t;+Txh{um7|)xhnO{L#mm9(NWdI`WFdHAHv4|0 zpK9UJ1;0$6CZA7*c_|-s^tLk|q7+z5Qn2^j&(S98lDQ|`PdrgpS#r<4>auKU_^fl^Gz6V29DmSb| z;M$hYM+F+*`|a-zX+KvYFNIIFwyX6x5*@E+8AHRI9UK+fLC- z#iX#3+p&u&#;%1nX8PBZ5rwF%%Z{&y-20z18Hd;1fAC=N5UmsTyG0rvQW`e!u;)A? zD75kVzOw$@r*nap?uft!O4`>l;a#5n4ILQRvQh0YTrxjPbM*T{f&WL|+xzUUhTD`m zlDU!7Wy*WsMy4iPB}u@6MliGmCG_*kdkvwojbRqND#&LmsOFm1f&T26S4s!MR>w8L z5_Q^$$n<=zKQ?^U`InE4B0anJq=i@JkgukXTh(~=yf%F%M=bE3%ZXG{Y zNHrXF&CqwC$;GUEk41q|rjnVJjg@^!w{FOIRO3bkZDeE+%P>no+S1GNN-+Fjv;32m zhV5Wt&^_e&`8M8{}6Gy zni{iS!N!rIliU4TL$ED^k$%B(x^P~1sBXo+LP$E0%2#VivE z^lI_0|7N`nraMcV*XTgju;rg^&T*cQ(b%W-M0tqMFkw`XcG@%#)_EP|Th2Q?#otZ+ zlM=I`l>c2E{yTKC#cr*^N8Wz%`_~bNnTL%_e`%OP+Gy^RB^BqzN9EckoQ+*J4&OR1 zlSOAbb>*sNiu_Tx&AJ%BC3)e)&w@5v5+w{9FZo;C)G0n`sh+nr&#P!U>2s%L?#3ip zwdBR{Y`wc*$Rc(aD!lUYr7h%TQNDWXRYtCimCiA>&*ei=%U2Q0By;B-`lqeRnG&hYX*YoGO>i%a=~{ z6FwQ6>dsZ$^y9$M*0*6>RvK&G8zrXkqn3v_B(6KG4cFTSSv-=^+JC~~y7qe`eG8X& z#_lzX-UNH^U*#NLg(mb<{LodGGatS!nXY&?%XTkQ=l+}+FDXFT=P^E;-`7W_NPo~cMEj)blI_yZF)Jo>LV zrIj*!&Hv`QSPuQWw4UxaG%K*?XIZZKw=l9%ruC{+L@jIIYtC*NORuD@*E#cv!|t3D z3=>bc{+)63=8YISYR|{-tMJ%%HM|FFBU%)$eY_JvV`!>YU1N4X(8K(}OZuK7?IOmx z-N~zi1`@6XG4qzmDqZ0e5rx5HR>uzth$?8qq5((B6Jka9FK5X4UQ z61QyaZZz#FiD#2~D<8UAcUDwPjb85H!QEmOdsd0pCSuex#c02?c%?L*{{E(#p-70J zrH52~d@0;0S4$+YWqSa(;oTr}_l%+WT><3!cjIKTOw1poOhl=h^xjnI;80%IOT6=I zp_qYh;Zypv*AeG~oL}c3DxfJvJ-1+TIKX(Slaj_CGuILO`i4UR#gO`mME+fV_gJWo zFQCka5<61aV*rG! z@DPYAvBVK8IPk+HS2)=VK$ZW4QWl-S!jI);dKMPN|GrHuiAMn~I2!lrYx}|Qt}bAR zFf~z%#U4KF2Uud-_%>GFO3rc;W_o%?k%=h2N$83>Ag@Sg5QI0`r-d<~@OnD_~)$=&5>g(FE<8BM)yv-g^~KAJ~ZR?G8>ECT#|t{+p?(voQKp z9}}JBrEI>TiYi7~IS9WWGfUFWiC>Cp$#M69ZFzwrhl+b8=F+}vE$zfHagON$3b(kmy8u<_}C?-YeLdhY+e++jZukRjz7zO*Y+i1s$6jt@=(dKM=Fc6xz> zjArGi#SGjucsMxn8r~lL7taYEJ#sRE&!P4)^ks~2bS>0jcPH5Zoc-WorD%X9yEACSBz^%-L(~0hc^Lxi z@Ss10SkE#$Y*PL_PnWaHsSUKh7l1`zdEnf~fw42zYTIIkE1(kyaNdx1a^mOio%F%c z0Rjy_ows#HOg28?z>&TP<{J1ehy8$Z2cyvSy`SmAO1Y>nm_u@lgKGr~%d5UV;Lz9N z`->I<5ZDDFrzpi?QrZW#OYZiWkJu@Vkp?+mD9 zq=I*4#<&285I?+RGBXpxX&D$^zIsJcXh2{L$fF92vT7;9nD`YE_!W;BMM{1A`csf%F$6#fJ@OELbVPWIrFb%_!69FeVszj2BZi>3^?3v#}o<0Ey5PUAuU}D zBOi{G_2NNUI;$=$Vk7~z!O~0!=S0#2xN{w`RJx7zP9cqCy9{kh(zbW4;JfzU;FEKM z4hiq)EhOOa`x`(&n8u;&_4hs-1@j4ZykaZ|!mdxkf4`Y3rwdKjoPWap8C5OTQ{3-2 zHA1)GwV^60S&A+VcX|`dJP}dRl2?B<97*di{uka6Ul@Fmg0SWZH>aD9TI)9(wA;Q2 zp_oeHFW5S~iMfeQ2^^$P3=BkJ)1D@RbHELOA$YDZC7C>THbsb5Lu^6mHN3(07c}cP zlX`t#(iby+1vi=5o^%dDLAy>R5!#lE+Lo3nC$h@ShK{Qw>~SVUiOp*o2O)dPq3#xLQ;7Cz1a!spBGjyWrr0 z{QUp_SbeOu5N=0{hlQuMdsuAfBeKiUhk#w*{rvqYHX|J`uj2n+EV1s=A3wZP{22=t zo2$5KTIF!shEYB?YrFvDqy~Q*@ag}HmmFbwq@`*Y9bDV4=0*DaO*hi#udfCn43OR| zVgSP7X(O487(n7^)Rm5k9ypNktg@m)n&bnHe%5gG0~RHC!KORTD7l5w%&oYCnQ5z- z&KzLGO_+|FoBw`p7s(ky&d$M+?iSNV*a@(6X5hh^WW7L$?dI@<@U>z-&JNGD(Ug*f z(maLEH1eU5l-wtyDY<7G)`wq?9(|C?CpU!ODbp2eL>PVm506uo|FtT_H!F&=!P;zfUvoB|>j2pm8~KAQZb=Ba5>g!W%bzbKcK~rh8hMMMD==5-W`_!_kWF{5>HyM4Vh<`qOa$lvGlQ86 z*l38y`TtN4DSoc39LMTwgi2)E?X0Y2I9>1zFSV_ffO{THY;laRKNCa*>@W}!0TwTN zB62=}8dniQ#T`Y>y64VS-7%ftuyfF}WXhG#tx_IlL=InfxK)7-oSToeZxxLcK2B90 zuCkYLn?CyZbQE>mMlqAwTXySq+eCTq5x8s6cuH$|m-c7-dgSgxGccU}9@kuxA2K)G zx742ka&C;L&vXrj*+gL|_fvA3wAr9%JVL1M*D36LsjmrGysez4FqEuaMpC&V9E2;i zsU95EN+x+e+BT*NTYCK^A56~3@HQ%SBcL3t#Fm&SFJu@ndfj(S*7!!Mk)~#N9+jdn zUV&W31U;>#rLIp9uqO=6WirIZq0`25%A^R#&;16B;V#nS&y?h zav6r<5G3+GCT9P0)qW-4OHspT*PhCs?c+DRP*_$>VY#)lW?098EH|`X|TruW}mudk@f<>!C{vp;ih|cvd=be^Vl_e3^TUyHs(No?^e((uwI~0sR^wA88Uh2}*6yWqvp{Hi`SE zS9`|$-B9)g`^O)PJGc^G5(2aFyNexaO!))y7f%#xb1`^Hc_ji}d!Cb197T~AJaqGo z`Rrf9sTQ2?A#T*PQ4cSAWd|SLU@4B{EN)L=GAMDL@!7s#lIusMGD?ciO>P$~yL*!& zgogB;oj+!4MGom$o&8+>Q|22=V6=y_Pk@8Cy+{^rGhm!CKF}g`2saLI`LNo1qleO_ za-YsGjxeogsU8wCHXIzO%qctX93V0H+oF$n{mK=R3Me6N*ofj{^oLv7KXJi2&Ut6M zUWjb>_~Zr`$nMcm9yzBz4{B+8U}0(0U#=^guu|q+1!xT*;NxEZj<@1cQtdLh)hKdu z>^J0J4baj)LQgOVc6^JnzkLAh81xl-U)=lpCsJ{?{w*!}JdqI+avUl&<7E@k96ZJN zC5=v`!~Od?ee0x-UcTz)Q}|ObBeX2K&T}F81}gi;b|P*n{h$vFHe6~Q_o5O&4bX5L zYl6-hT9;$>Df2Vj`eM!%BlxfrFpJV`#W)i~M}Cs&kDp&HE)4R2zt|;;+T&(sTIV>y zh`D()uTgpdIRWlqNUCkp3yg|>4H%BWaf4%`@Mgt=<&VW;IX(;?`v{yZVdpWzde?=x zlz!~)VtXFo>MM=nqHC}^@W*EE5(Rx%#%IWYdRfR+zrMajC4PM;W zc3B@rQ6ClSi$h7HLos8oPhHdmt>N)Bn_i-6BZgtlglhuI&PF_^sLp4Wlz96$>PA!C zh&+i2$uiYpE!K%{1Q@=3_=h_lNi5N~PDL-g9~;LnBb?$epY!nnZ|5ZbW1!FQEB6+f z&dJSh7f`zC?n(a#`N+cM4UD}>){5=#fwJM4Am?1$w%@iY_8juLRwQ+~P$cg{A2A-d z+7(w6M6eqIiv;nc8d*=`Ux~x!)nT)6y!+|OyfFf(w0Hp|a}1c;yhdM7M5ae+>Q@?&jksCrH ztkc=hFKBjw4&oFqO$>HTaI`KYx$gDyF_9gJ_LP4A$h4REA<0ANNw}RW=}|G~z+R&0 z0OK7x0g6ZIk>_zqNhM7b0ShNVlJxPN!Vu2mG}7mFMc*LA#DUBCf`4?k{=_#O^-49o zK888h%INqwU)K)BGXgh~<}ibbZ#QcIr!Nrc2PYU8QOg^WymBfJ<=~`=fce){&oy`L z`H8|17q=r6RgSa_1O`+L+#h(;c06~ibf$dHy|Pzj$@-V6wRHz!H^`;JlCiH>NI~9)@=pA)Oys?LJC>7_ zg~YJz8Dp0RvOSt2Pg+-vy3sz`@JMKmU>BAWlpLy^2O5PVf8WeqPyqp3^7s?RK}{Xr zL8{|q%I-T9XWTy220g65;e=Xk%qi%|A=apMh#%v&-%|~GooU9u@a{k@Bo`wPW_%Lx z58KA^4KG;Kan4m+n|u%xr{Jv(Wr_jB)`0<7LV!qe3&F302BlXh+CU}7tF`DZ=vCZ$ zc-?f~^EQZ+xyYRVV)HI6_;>vBF&ue*x?UOal$cd^y~-inzasX^>&K5xn@g_z0^75u z<~I%p?{!qA<}$E~8iMRK2y-#ch*wQ}KV%NgU%_#XD@n!kffP5QXN3{dE1%c9+ks~367_NP5S3jq1^<<)_ML_!}O z&+?gu@cYtE0e3x{h?q!6xT#^Yh>Tqsmy(k5&WT`$SI?UVw0C@V%+6Dsdn+et@YI?m zgt**+zUb67l8?;C;Evq3Zra%dJ~>$3U;zr>X;fqAl?t+aHS=k^d8BNkZgT|PjF!_J z7T<9toc*A#uFxm(`&iY-#`K9>>ZpUANNKJnwahu5RLz}tF;L{^k9R{|w1rX#(bI@D z$JAaT<2+K$=2lAbp?!u8NQ_U$;?X)3HVzrq|8|FrBE=+pD3M%-8=vD*XrJWUEsvs971YE>9us?OOn1lEt@SpGb3|MhpN`9 z80+O)Sk_gZcts;Qys3^MBCppqrZolWFHeHhwOB6?k@y;XviVs@>#Y)UvV;#lbg&Bj zLl!#xbpo?Funr_AoFN7=klzP|>8H?p%b{ymJymH|@L;J=+e2iq>?xVO!8?-V^>jna1nsHCz zWAzu*uTsllEve&0RyrC^ORMu19X{16w_XyrM=*7&n#ptoouI0|3*LYNd27x$m%+s* z*4`c@j^axpQcP4dk)EcC*XAtcn8xnotimC}G{oPaOc|%DBYd^sxC?!G{E z5)+sDcdpug*|z}e_~uqeO&9crGXwV)TXPzeO=K%7 z4rNxl(GRzB*A+bxn@F+WVMA3w^W240J|cfUwm%lDBU8Zeqy_AOvZrg8;tV&ZUL?fE z`uO|fv1BTL?{y@^Qf&Q53Ize8WUtri*8f?&u%n~U9?ts@7$O{dS&NZVs?xJQxwATo3#r5KPjQwmk;EQIi>Zamn6N>dmklrDTl?%owVhR%qW+1%#pJi?Wed?B^li;0N_*g5{DhR>AO@Vm45C~toF zB(U%+^l+9b2s4UO3X7lEJh(eY9S-#V0CNJ;smayXjH50D2BbPNl{z$TAMB5Mp7cR| z&z`u;l@OW-u$OM3*OL_z`T%9ggPS)w&TK3HKKl3`c2a_NA^HOBioMkb?%SO>AlAjy^M?l0F3Z~4Uy5I zIA%CO8tZd;h^a6F$y5&^Od9ULgx?s$#E+TTVIM4`A?>L_^N;Z>X}L_hrnHRLka(># zCdVWNmlMwrzisf(FI{a%9YUh1p7~D4TuxsSln|qh*ad6~459kE@%94 zD?OLi*6wPAUiN=?Xtzxmd^p&%72$XZQtLStS=r_J10f0={!Jg@?gG>ja%vJx^E5j< z>@{FpG_|!}!^NQe6~|wq>#mEbftARqMD6BP(j}>Cc}iWIr>mqMn=nD z(hWMAuObpS0cCArvCeDAg>Y&~(?dKy^pclfT+PC_KE%PFNL%Uly27;D{!KYwKYS1>Xr6H_6*DMVG$LU# zGnl^z{&3#xJKy}ors2{bz^R1Q1RdjkPsW0K_n@uN+}u%C+C^&PDk@mF!1w}yf|hZE z?1;A7FQW@E{61>s`etHc`(ZY2?v5Vxxg>!ns8Tu+ z7gf=M)B-M$k)a`Jz|e~?8N0Y3%Y?PnKUY^XF}B(Y-qZZVbpIhJS~TCJN*!159+6zT zQ+dIk@m9b-kDlO%&1JG!`87e{rF}w95$-C}>$%QF*}SeCw0sh%Agsu}`dCZ(;kwgD zgefpeA4dY=kD77LeT27g?8iLz)qnUqHQ6T0#$k%vGj(n{%3&BjIG}_E;7-Ci=57)q zglwe509A5HSpv>4QtAagkpKK;1B?~4ER{S;wre&N;(4&ZVi zItpX>tQ4lrL!{(X)ceY2Mj(8Pzu#kVhLB(ayo>oZtE|IbyqdHCKFk|8DJ!Z;t`DdXWW z(b7tb(LZSc@JjMawX<71OF-6qj$aLQ@dM9Z0FDv;jI8O5ywZ+H*M9N!r~hXhRC^{0 zxJxF{^EIwyVqC;S@Js+3y1mM>d-ak7=7Ba(&j?0}jVveVTtL zI!48@RXG^oDa6OmKx$HU7suEIh+HDt87gY(ZvL=S@%Tk91Q4}O84)K3r4b!SV}M*{ z{pQKZf6yO0Wx*5=7!kT5TS;xbw@ak`OQ5IjMMg0;GLqcCfAWr)cy>jTxf$l@OxCwD}2psWHO2c+Z=iDN9rI-`+Y@vVOPJ-sw!&jZtCo*KVw*p(7 zFT-B|#l}c;GX#bo(Yab*bl)$P(1a5)Dz(%z10l}*{IRE=O%Wgif-1Qep&_ABh_<&E z^$+SSSQzE2AJ{!cpGN)89?u#rA?%D2;^Jh)7R33)#CPYa;mD!rg0E2tRyijK9w0fL z_7>Na4!wN2h`n4ZsK9%Is6v=weeJHDJBd@Dlf)=J zAzs)**Y_AUkp5ZWVB<_nVHX2-en1^V-q~dD_5_2cj!Vtv$kbUdS!8i_s=M`=FwU(z zADegRLUDQw{x&H{8y9Hhcr*T|5nY?{?C$OEHg5>$%fhV%dWG0HDzhpd_4Y z*|2?HaT#Z8=x}4{5s@Fnd3_ljc@P^n@bxX)`#IwS>e0>Hnchp(9Vr=g0_KL-)-lE& zy5e0+7b$gmxOf;H_Iz*YI=~k|k&~b+D}UwIcG(PHC=VbRhCM(3a9>sF9eSi|J1Z`u zqh3QJX3YGBXPG&?_H3&4O6mz3v|Z)nQPfffnHZf>$@CUQce8B}blqJIE4~tFgc1B^X8*X@eXpgV8$)z1!T=#34ulpJp7@!M221lY)KOhPbZ5YR@6N#4Z zrNa$e&YO-y>rWzD(u7^DWWqx}Gx|ty{h;uD0>*6~5>7;B3Z^V2UuQf}Uj^J87y>KT zkJ$VHrM67zO&t4USa#rgt#U=uF=D8h;vhZ%Se;7n!W;PsZ5> zCx5W+_3oAn0-y1E-XE|ty%@yR3Q&P+0j9cWE$zbxc_ShSYh zUb#kxaPE}IFpX)yVV0~by&bC* zDTSdTgu_Nh1?kvB3NN#lw6Cl2-`~c6SWVr2J^LG$c@*&bkmO_3_TlVXlmGq#ef>M> z-g^hl)gH@xe_(QScoRjrkt?Fr#skPFG0MqSA7fLX`T9V%lp&~qQXmN=PRxVlXIbTYg@9E!P91dxfMr=B2G&AwZsBRId zZW1K`6KPV+(FqTEcqE;o=C1CZEbI@Yc*aVx_t~(uuvI7E6~Hl6@nt&C4rusWwShOh zw;7w=yPwFC0VCtx)ydC(*!>Q9%G&d@KaOuACX`&#dd_8c4_wi-Tt`XapzM^=^dg-Ka^ zzik8UGtzd`SL>~!{SNl0XuN5ZHTj9?fFBtDI~b&TV;Odedg&!Q`_eO8v|>aKNDs%f z&&#uMlLIABVU#g$j*um>W3Lzb|*e{tbwJ$ z)~~<2aDat+=FV@_5)rrgo@+Otmj_Q3IX$K`5$w1NyWKqs1iZ~2PLOYd7`x9q!oTLi zKh?Cr#rdD?`XL29H6pHd7n#nB|M2)_^eK%&%fn zX&QL5jj*i((?4p~y-^F24UIz1GX3~UxH!UGhJB@H0bAZ_SVlH9c6#N+hf!&pL;zY%`Tlc;3{sBXEj(+tS8n5n;wM zWWXH+gU`CH?%!Tldr@Ocdb%&#pfjg|&L%(1sWq*d$@S8Oiw&6oJMhBxWj>tmtvpu> z+)kfKSoJWn-u=+^ZHEagNQv)mo?c~h81lM$p^h(_ImEU!IqcTwETEC9{`x|l#7TGd z2fLHP=u7!+Y^>VjPOZ&fGDzjFzX58-^aBZr(4n%F%PmvWaMCF9pv1dp1lAj`=R7j( zk+r(RaK$m**`Ei?{6ed`^e)x(j0W>LBmDdMZQoKa8J?D26am*NG#J4BzmJd0xNAI_ z=dvtEp}p{3zIV@h@CO%rGloBE1c#9&+CqD%&FS08GxyU@m|41(GPbZW80z8+mEH|G zHKzS%nT^TL=}xDWAIZNB&z#}@M3X8ld|q8wUw>IlHR^bE%$q`V8K>H0cb*hCxh%0! zQANeF;&OC$F5FXEU0lsH`X1;b!gy8`%dj`LBwbs}ZQyl+N?LgWJS$WTv2bA3jG&%E z@`bkLFA}B%rTm(mKc-Vn%~sw`Ybv1St{eW%s&>k|M(e)$(XrS+^(j|Ry3w7F_wJ^b zEVK%ktQ`z{r{QlU&2Q5--#32u9%J8=ki`Wbl9>^7Dd2oy zB*FxR!=LZ0HXw69yY-d9p+b82bkMvOPYtp3*g5%l~n!K1t z;V&Z>g2}K&H!DEld#$FD09D}XFL!o!LOLQMEG$NB*4T>CH_0yhkZf|qi-A#2c9g+s z=h*KD-!`m{)eD>=cfB4N`H`)$57nK$3w|e^%1!;&QvPriV++gl157p#LV-xufkux9PVTNKHdvIlHvc9MXCJDGWDwR! zCNYu_&c%6|sXa91CXv>Z9ad&gZ}oCzyP`-=;0=(;S}Nl zIoT}QzqlYp#xi;gECU?AgM-+B@0`}{PyF>-Rb<#cUz->C=XL99yLx(FR7<2W4gjJ% zcQ#X3wvykURh!~{s!zbq?ga|ScFrYUMuc<83=WxwMunzH@qQ`^73ua=6KZmlh3+)> z;|E#Xoh;r}lFU_4MRH%P(#B1muPswL{A3QH+FO0D4mEktMRDZD=s<6*&AKNQc^jSj zPKmSg1zNrVv_UcZCQcA04tTJ-M!2P!fYXy7k)v+pcnkXrwxctO&6J zF)~|@4}BOb!A1gOeyxn&)Cr-6@;@v>W~{LM@vTcAh|#`B@+vT8#dL=#k3zjRP|2ex z5B65eL#Xu?b^4}L2xg+*>dbBVYjl^vhg_C<%7QEOcf>CvmI=jzMzx`JAc+G|vtY;-d7w?a zMv*1*n6J=`Ym{L|OXl5lrGEQnb&dNAKZio}0BXE^>G(rnblU;W7}4MN;T!ZHg9MgPd#R9!(gQK9(Ut{X_0_8&WE)5bQT|_Mv3l%WeU6=NK?2-A zL2x(tF8!bEPF7R39>Cpwj#`E;N+wh2+5OW1bTy;-0l&%@Y4(WTc_ z5CeTH=h5)s$>G4UNMwXT-G7`PfN2K{=-*{>MSr*oP7Sg-3uf~&76=K1{6&M-Kl8cjGs24RAF zxkf@noz&RR8(BC zyx6@-=qyLYnl~G$E*v4pNMkCpKPa?jXn$=B9h(%q%qOeAf{O<&UHsO;5`45s=yQy# zNq>IY#hA)%zu#tU;g$JUSk7qT9jKBmG*o42U}U9JE_mIl-zYZGSWEHM!KIBmoWda{ zX+sxO*lp)psvFeLaj*ZWf~ep0vm(i#`Krrq?O*xPldoBg=_v^Q+D-cb2Rkrai)&d# zWcC_iwnS90sZlDME~lrTy1K(P<%LCx#9hI2Nk!?K1!0@H=pBCPCty0@HA4s@*zfqV zFkt%<3srIj;I;ewVl~0(RuS#kUScuZ=|4wT)U1V#dj0x}V9bq^mzFmCDblR}T94i) zOnJ0MyIF{}%jUJqH3!;)s)b2DZ1$om|O2pJ1J&oMYLo z95cnMJ_s9GOvi`qs;YYEa9Wr4)gN2&**VY|)N^??Eww|Q6d%16y))gi(O>v|WTH4K z{sH(yGWE?SGhN{Trpn*n^2)L``g4l%75R`EIXqiV3z;gjASj@2v(xZ+AJpH!)cR zp$NH0ECUB7CvlZ9J`PnbLYl&iHVn`05$jIslW0lSXzTBPe7Qsk`El5{=L!EXPM$df zCx0FQ=#3)VoPm=-@JVD09Tb$#-)-MLX&cs!nK+~?@ z*xA1onXKQbQ&(5n^8dP0rl`s=ZZ@ve_IH%#5VYVU=fK3DPFp%=68;dZsTj2M{o204 z+xPFEAv_6oG5BS>1Q=%|HAub)SbDR7x-t62BPn?LW9jGyYF%_kM~tb47JHXlhKmu@ z@DM_?5rh9-AYL?&F6znG`d1|e-=nmJK(Ijz-OY%5-GM{nL~3YK+QdW9ga-J}Kf9LX zHpIhi_W}$H^anTZ`n79fYk0(mLE@lz1|rF=0`JH*8pB_bC3l`+_A5QOI`@QNdl1LK&iZ!GZ4X?_h92#Y|j=Chh|U zxS1J2KT51}D&a-o7ZAwH&833G$2xqQ>p?*p3h@qELc;M*kf%d~W*q-ppKp0u+m2;s z@NQ$j3rN*2k(#08+of-9*;O8zqc)YUWkH*WZm`ilH?e0Poo`>Gq`|9*EsOx485-k- zLc~3&#~OzN1~>gEmN{StBp3teWJk<>O{H>`AXP+~JNojmDq7J%HiO88+z>nTAfgFK zf8dtLV>K8)FCVFx%Ai379vE!!t`umfkEsqQ;< zuZ(DDi;}om*4~IQ*Q{`5`Y2@?#f)B1xlDH2O#YtTIo}AS+w(C=Ar5sGf9nqS{; zcF(4c;nPd+5;$7S{4^uI){rrGqOUmArnnGEaEW)`YDvk$vaOqoPk{TPezjfN4TTok zXt~eGbGji5j!x_7F9au^39w_rgGq)?arD^ka>?|=zqJ?XmVYX*m@W@wsg4Wy`#GB@ zJnyOG4{{h6NYhnw6q~9%Mp`_-Bg?yX^0>zFqDeQ!NtEwopIC2CB52{~?}(*s)PT&6 f{D1#>y13lvk>n}mQ#n2y__uf0e&a$zm$3f=TVoZ3 literal 0 HcmV?d00001 diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 7e239d1768..ddaee104c9 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -37,17 +37,18 @@ execution. [Kubernetes finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) make sure that a reconciliation happens when a custom resource is instructed to be deleted. Typical case when it's useful, when an operator is down (pod not running). Without a finalizer the reconciliation - thus the cleanup -i.e. [`ResourceController.deleteResource(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java) +i.e. [`Reconciler.cleanup(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java) - would not happen if a custom resource is deleted. Finalizers are automatically added by the framework as the first step, thus when a custom resource is created, but -before the first reconciliation, the custom resource is updated via a Kubernetes API call. As a result of this update, the -finalizer will be present. The subsequent event will be received, which will trigger the first reconciliation. +before the first reconciliation, the custom resource is updated via a Kubernetes API call. As a result of this update, +the finalizer will be present. The subsequent event will be received, which will trigger the first reconciliation. -The finalizer that is automatically added will be also removed after the `deleteResource` is executed on the controllerConfiguration. -However, the removal behavior can be further customized, and can be instructed to "not remove yet" - this is useful just -in some specific corner cases, when there would be a long waiting period for some dependent resource cleanup. +The finalizer that is automatically added will be also removed after the `deleteResource` is executed on the +controllerConfiguration. However, the removal behavior can be further customized, and can be instructed to "not remove +yet" - this is useful just in some specific corner cases, when there would be a long waiting period for some dependent +resource cleanup. The name of the finalizers can be specified, in case it is not, a name will be generated. @@ -67,95 +68,140 @@ When automatic finalizer handling is turned off, the `ResourceController.deleteR case of a delete event received. So it does not make sense to implement this method and turn off finalizer at the same time. -## The `createOrUpdateResource` and `deleteResource` Methods of `ResourceController` +## The `reconcile` and `cleanup` Methods of `Reconciler` -The lifecycle of a custom resource can be clearly separated to two phases from a perspective of an operator. -When a custom resource is created or update, or on the other hand when the custom resource is deleted - or rater -marked for deletion in case a finalizer is used. +The lifecycle of a custom resource can be clearly separated to two phases from a perspective of an operator. When a +custom resource is created or update, or on the other hand when the custom resource is deleted - or rater marked for +deletion in case a finalizer is used. -There is no point to make a distinction between create and update, since the reconciliation -logic typically would be very similar or identical in most of the cases. +There is no point to make a distinction between create and update, since the reconciliation logic typically would be +very similar or identical in most of the cases. -This separation related logic is automatically handled by framework. The framework will always call `createOrUpdateResource` -function, unless the custom resource is -[marked from deletion](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/#how-finalizers-work). -From the point when the custom resource is marked from deletion, only the `deleteResource` method is called. +This separation related logic is automatically handled by framework. The framework will always +call `createOrUpdateResource` +function, unless the custom resource is +[marked from deletion](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/#how-finalizers-work) +. From the point when the custom resource is marked from deletion, only the `deleteResource` method is called. If there is **no finalizer** in place (see Finalizer Support section), the `deleteResource` method is **not called**. ### Using `UpdateControl` and `DeleteControl` -These two methods are used to control the outcome or the desired behavior after the reconciliation. +These two methods are used to control the outcome or the desired behavior after the reconciliation. -The `UpdateControl` can instruct the framework to update the custom resource status sub-resource and/or re-schedule -a reconciliation with a desired time delay. Those are the typical use cases, however in some cases there it can happen -that the controller wants to update the custom resource itself (like adding annotations) or not to do any updates, -which are also supported. +The `UpdateControl` can instruct the framework to update the custom resource status sub-resource and/or re-schedule a +reconciliation with a desired time delay. Those are the typical use cases, however in some cases there it can happen +that the controller wants to update the custom resource itself (like adding annotations) or not to do any updates, which +are also supported. -It is also possible to update both the status and the custom resource with `updateCustomResourceAndStatus` method. -In this case first the custom resource is updated then the status in two separate requests to K8S API. +It is also possible to update both the status and the custom resource with `updateCustomResourceAndStatus` method. In +this case first the custom resource is updated then the status in two separate requests to K8S API. Always update the custom resource with `UpdateControl`, not with the actual kubernetes client if possible. On custom resource updates there is always an optimistic version control in place, to make sure that another update is -not overwritten (by setting `resourceVersion` ) . +not overwritten (by setting `resourceVersion` ) . -The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent resource are -cleaned up in `deleteResource` implementation. +The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent resource are cleaned +up in `deleteResource` implementation. -However, there is a possibility to not remove the finalizer, this -allows to clean up the resources in a more async way, mostly for the cases when there is a long waiting period after a delete -operation is initiated. Note that in this case you might want to either schedule a timed event to make sure the -`deleteResource` is executed again or use event sources get notified about the state changes of a deleted resource. +However, there is a possibility to not remove the finalizer, this allows to clean up the resources in a more async way, +mostly for the cases when there is a long waiting period after a delete operation is initiated. Note that in this case +you might want to either schedule a timed event to make sure the +`deleteResource` is executed again or use event sources get notified about the state changes of a deleted resource. ## Automatic Retries on Error -When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. -The retry is behavior is configurable, an implementation is provided that should cover most of the use-cases, see +When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. The +retry is behavior is configurable, an implementation is provided that should cover most of the use-cases, see [GenericRetry](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java) But it is possible to provide a custom implementation. -It is possible to set a limit on the number of retries. In the [Context](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java) -object information is provided about the retry, particularly interesting is the `isLastAttempt`, since a behavior -could be implemented bases on this flag. Like setting an error message in the status in case of a last attempt; +It is possible to set a limit on the number of retries. In +the [Context](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java) +object information is provided about the retry, particularly interesting is the `isLastAttempt`, since a behavior could +be implemented bases on this flag. Like setting an error message in the status in case of a last attempt; -Event if the retry reached a limit, in case of a new event is received the reconciliation would happen again, it's -just won't be a result of a retry, but the new event. +Event if the retry reached a limit, in case of a new event is received the reconciliation would happen again, it's just +won't be a result of a retry, but the new event. A successful execution resets the retry. ### Correctness and Automatic Retries -There is a possibility to turn of the automatic retries. This is not desirable, unless there is a very specific -reason. Errors naturally happen, typically network errors can cause some temporal issues, another case is when a -custom resource is updated during the reconciliation (using `kubectl` for example), in this case -if an update of the custom resource from the controller (using `UpdateControl`) would fail on a conflict. The automatic -retries covers these cases and will result in a reconciliation, even if normally an event would not be processed -as a result of a custom resource update from previous example (like if there is no generation update as a result of the -change and generation filtering is turned on) +There is a possibility to turn of the automatic retries. This is not desirable, unless there is a very specific reason. +Errors naturally happen, typically network errors can cause some temporal issues, another case is when a custom resource +is updated during the reconciliation (using `kubectl` for example), in this case if an update of the custom resource +from the controller (using `UpdateControl`) would fail on a conflict. The automatic retries covers these cases and will +result in a reconciliation, even if normally an event would not be processed as a result of a custom resource update +from previous example (like if there is no generation update as a result of the change and generation filtering is +turned on) -## Re-Scheduling Execution +## Rescheduling Execution -In simple operators one way to implement an operator is to periodically reconcile it. This is supported explicitly by -`UpdateControl`, see method: `public UpdateControl withReSchedule(long delay, TimeUnit timeUnit)`. -This would schedule a reconciliation to the future. +One way to implement an operator especially in simple cases is to periodically reconcile it. This is supported +explicitly by +`UpdateControl`, see method: `public UpdateControl rescheduleAfter(long delay, TimeUnit timeUnit)`. This would +schedule a reconciliation for the future. -## Retry and Re-Scheduling Common Behavior +## Retry and Rescheduling and Unrelated Event Handling Common Behavior + +Retry, reschedule and standard event processing forms a relatively complex system, where these functionalities are not +independent of each other. In the following we describe the behavior in this section, so it is easier to understand the +intersections: + +1. A successful execution resets a retry and the rescheduled executions which were present before the reconciliation. + However, a new rescheduling can be instructed from the reconciliation outcome (`UpdateControl` or `DeleteControl`). +2. In case an exception happened, and a retry is initiated, but an event received meanwhile the reconciliation will be + executed instantly, and this execution won't count as a retry attempt. +3. If the retry limit is reached (so no more automatic retry would happen), but a new event received, the reconciliation + will still happen, but won't reset the retry, will be still marked as the last attempt in the retry info. The point + 1. holds, but in case of an error, no retry will happen. ## Handling Related Events with Event Sources +See also this [blog post](https://csviri.medium.com/java-operator-sdk-introduction-to-event-sources-a1aab5af4b7b). + +Event sources are a relatively simple yet powerful and extensible concept to trigger controller executions. Usually +based on changes of dependent resources. To solve the mentioned problems above, de-facto we watch resources we manage +for changes, and reconcile the state if a resource is changed. Note that resources we are watching can be Kubernetes and +also non-Kubernetes objects. Typically, in case of non-Kubernetes objects or services we can extend our operator to +handle webhooks or websockets or to react to any event coming from a service we interact with. What happens is when we +create a dependent resource we also register an Event Source that will propagate events regarding the changes of that +resource. This way we avoid the need of polling, and can implement controllers very efficiently. + +![Alt text for broken image link](../assets/images/event-sources.png) + +There are few interesting points here: +The CustomResourceEvenSource event source is a special one, which sends events regarding changes of our custom resource, +this is an event source which is always registered for every controller by default. An event is always related to a +custom resource. Concurrency is still handled for you, thus we still guarantee that there is no concurrent execution of +the controller for the same custom resource ( +there is parallel execution if an event is related to another custom resource instance). + ### Caching and Event Sources -### The CustomResourceEventSource +Typically, when we work with Kubernetes (but possibly with others), we manage the objects in a declarative way. This is +true also for Event Sources. For example if we watch for changes of a Kubernetes Deployment object in the +InformerEventSource, we always receive the whole object from the Kubernetes API. Later when we try to reconcile in the +controller (not using events) we would like to check the state of this deployment (but also other dependent resources), +we could read the object again from Kubernetes API. However since we watch for the changes we know that we always +receive the most up-to-date version in the Event Source. So naturally, what we can do is cache the latest received +objects (in the Event Source) and read it from there if needed. ### Built-in Event Sources +1. InformerEventSource - used to get event about other K8S resources, also provides a local cache for them. +2. TimerEventSource - used to create timed events, mainly intended for internal usage. +3. CustomResourceEventSource - an event source that is automatically registered to listen to the changes of the main + resource the operation manages, it also maintains a cache of those objects that can be accessed from the Reconciler. + ## Monitoring with Micrometer ## Contextual Info for Logging with MDC -Logging is enhanced with additional contextual information using [MDC](http://www.slf4j.org/manual.html#mdc). -This following attributes are available in most parts of reconciliation logic and during the execution of the controller: +Logging is enhanced with additional contextual information using [MDC](http://www.slf4j.org/manual.html#mdc). This +following attributes are available in most parts of reconciliation logic and during the execution of the controller: | MDC Key | Value added from Custom Resource | | :--- | :--- | From 33c2effb02f0e8e0f230b4cf49302c01af418462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 16 Nov 2021 17:58:45 +0100 Subject: [PATCH 0134/1608] feat: Add tomcat operator sample (#659) (#682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixed informer bug and related IT as part of this commit Co-authored-by: Chris Laprun Co-authored-by: Attila Mészáros Co-authored-by: Adam Sándor --- .github/workflows/end-to-end-tests.yml | 77 +++++++ .../internal/CustomResourceEventSource.java | 9 +- .../event/internal/InformerEventSource.java | 8 +- .../operator/InformerEventSourceIT.java | 8 +- ...formerEventSourceTestCustomReconciler.java | 4 +- pom.xml | 29 ++- sample-operators/pom.xml | 78 +++++++ sample-operators/tomcat-operator/README.md | 76 +++++++ .../tomcat-operator/k8s/operator.yaml | 91 ++++++++ .../tomcat-operator/k8s/tomcat-sample1.yaml | 7 + .../tomcat-operator/k8s/tomcat-sample2.yaml | 7 + .../tomcat-operator/k8s/webapp-sample1.yaml | 8 + .../tomcat-operator/k8s/webapp-sample2.yaml | 8 + sample-operators/tomcat-operator/pom.xml | 82 +++++++ .../operator/sample/Tomcat.java | 20 ++ .../operator/sample/TomcatOperator.java | 34 +++ .../operator/sample/TomcatReconciler.java | 201 +++++++++++++++++ .../operator/sample/TomcatSpec.java | 23 ++ .../operator/sample/TomcatStatus.java | 14 ++ .../operator/sample/Webapp.java | 21 ++ .../operator/sample/WebappReconciler.java | 208 ++++++++++++++++++ .../operator/sample/WebappSpec.java | 34 +++ .../operator/sample/WebappStatus.java | 24 ++ .../operator/sample/deployment.yaml | 39 ++++ .../operator/sample/service.yaml | 12 + .../src/main/resources/log4j2.xml | 13 ++ .../operator/sample/TomcatOperatorE2E.java | 136 ++++++++++++ .../src/test/resources/log4j2.xml | 13 ++ samples/README.md | 4 - smoke-test-samples/README.md | 4 + .../common/crd/test_object.yaml | 0 .../common/pom.xml | 6 +- .../operator/sample/CustomService.java | 0 .../sample/CustomServiceReconciler.java | 0 .../operator/sample/ServiceSpec.java | 0 .../common/src/main/resources/log4j2.xml | 0 {samples => smoke-test-samples}/pom.xml | 6 +- .../pure-java/pom.xml | 8 +- .../sample/PureJavaApplicationRunner.java | 0 .../spring-boot-plain/pom.xml | 8 +- .../operator/sample/Config.java | 0 .../SpringBootStarterSampleApplication.java | 0 .../src/main/resources/application.yaml | 0 43 files changed, 1288 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/end-to-end-tests.yml create mode 100644 sample-operators/pom.xml create mode 100644 sample-operators/tomcat-operator/README.md create mode 100644 sample-operators/tomcat-operator/k8s/operator.yaml create mode 100644 sample-operators/tomcat-operator/k8s/tomcat-sample1.yaml create mode 100644 sample-operators/tomcat-operator/k8s/tomcat-sample2.yaml create mode 100644 sample-operators/tomcat-operator/k8s/webapp-sample1.yaml create mode 100644 sample-operators/tomcat-operator/k8s/webapp-sample2.yaml create mode 100644 sample-operators/tomcat-operator/pom.xml create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Tomcat.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatSpec.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatStatus.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappSpec.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java create mode 100644 sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml create mode 100644 sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml create mode 100644 sample-operators/tomcat-operator/src/main/resources/log4j2.xml create mode 100644 sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java create mode 100644 sample-operators/tomcat-operator/src/test/resources/log4j2.xml delete mode 100644 samples/README.md create mode 100644 smoke-test-samples/README.md rename {samples => smoke-test-samples}/common/crd/test_object.yaml (100%) rename {samples => smoke-test-samples}/common/pom.xml (85%) rename {samples => smoke-test-samples}/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java (100%) rename {samples => smoke-test-samples}/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java (100%) rename {samples => smoke-test-samples}/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java (100%) rename {samples => smoke-test-samples}/common/src/main/resources/log4j2.xml (100%) rename {samples => smoke-test-samples}/pom.xml (85%) rename {samples => smoke-test-samples}/pure-java/pom.xml (70%) rename {samples => smoke-test-samples}/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java (100%) rename {samples => smoke-test-samples}/spring-boot-plain/pom.xml (88%) rename {samples => smoke-test-samples}/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java (100%) rename {samples => smoke-test-samples}/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java (100%) rename {samples => smoke-test-samples}/spring-boot-plain/src/main/resources/application.yaml (100%) diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml new file mode 100644 index 0000000000..404be8c102 --- /dev/null +++ b/.github/workflows/end-to-end-tests.yml @@ -0,0 +1,77 @@ +# End to end integration test which deploys the Tomcat operator to a Kubernetes +# (Kind) cluster and creates custom resources to verify the operator's functionality +name: TomcatOperator End to End test +on: + push: + branches: + - "*" +jobs: + tomcat_e2e_test: + runs-on: ubuntu-latest + env: + KIND_CL_NAME: e2e-test + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: clean resident local docker + if: ${{ env.ACT }} + continue-on-error: true + run: | + for DIMG in "$KIND_CL_NAME-control-plane "; do + docker stop $DIMG ; docker rm $DIMG ; + done ; + sleep 1 + + - name: Create Kubernetes KinD Cluster + uses: container-tools/kind-action@v1.5.0 + with: + cluster_name: e2e-test + registry: false + + - name: Set up Java and Maven + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt-hotspot + cache: 'maven' + + - name: Build SDK + run: mvn install -DskipTests + + - name: build jib + working-directory: sample-operators/tomcat-operator + run: | + mvn --version + mvn -B package jib:dockerBuild jib:buildTar -Djib-maven-image=tomcat-operator -DskipTests + kind load image-archive target/jib-image.tar --name=${{ env.KIND_CL_NAME }} + + - name: Apply CRDs + working-directory: sample-operators/tomcat-operator + run: | + kubectl apply -f target/classes/META-INF/fabric8/tomcats.tomcatoperator.io-v1.yml + kubectl apply -f target/classes/META-INF/fabric8/webapps.tomcatoperator.io-v1.yml + + - name: Deploy Tomcat Operator + working-directory: sample-operators/tomcat-operator + run: | + kubectl apply -f k8s/operator.yaml + + - name: Run E2E Tests + working-directory: sample-operators/tomcat-operator + run: mvn -B test -P end-to-end-tests + + - name: Dump state + if: ${{ failure() }} + run: | + set +e + echo "All namespaces" + kubectl get ns + echo "All objects in tomcat-operator" + kubectl get all -n tomcat-operator -o yaml + echo "Output of tomcat-operator pod" + kubectl logs -l app=tomcat-operator -n tomcat-operator + echo "All objects in tomcat-test" + kubectl get deployment,pod,tomcat,webapp -n tomcat-test -o yaml + echo "Output of curl command" + kubectl logs curl -n tomcat-test diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 578c6894cb..67998c7584 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -161,8 +161,11 @@ public void onDelete(T resource, boolean b) { @Override public Optional getCustomResource(CustomResourceID resourceID) { - var sharedIndexInformer = - sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); + var sharedIndexInformer = sharedIndexInformers.get(ANY_NAMESPACE_MAP_KEY); + if (sharedIndexInformer == null) { + sharedIndexInformer = + sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); + } var resource = sharedIndexInformer.getStore() .getByKey(Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), resourceID.getName())); @@ -173,6 +176,8 @@ public Optional getCustomResource(CustomResourceID resourceID) { } } + + /** * @return shared informers by namespace. If custom resource is not namespace scoped use * CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 64b4a8d753..9b61b18ade 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -86,12 +86,12 @@ public void onDelete(T t, boolean b) { } private void propagateEvent(T object) { - var uids = resourceToCustomResourceIDSet.apply(object); - if (uids.isEmpty()) { + var customResourceIDSet = resourceToCustomResourceIDSet.apply(object); + if (customResourceIDSet.isEmpty()) { return; } - uids.forEach(uid -> { - Event event = new Event(CustomResourceID.fromResource(object)); + customResourceIDSet.forEach(customResourceId -> { + Event event = new Event(customResourceId); /* * In fabric8 client for certain cases informers can be created on in a way that they are * automatically started, what would cause a NullPointerException here, since an event might diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index d290d06ecd..06a1238bb5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -13,7 +13,7 @@ import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResource; -import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.RELATED_RESOURCE_UID; +import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.RELATED_RESOURCE_NAME; import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.TARGET_CONFIG_MAP_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -36,11 +36,11 @@ public void testUsingInformerToWatchChangesOfConfigMap() { var customResource = initialCustomResource(); customResource = operator.create(InformerEventSourceTestCustomResource.class, customResource); ConfigMap configMap = - operator.create(ConfigMap.class, relatedConfigMap(customResource.getMetadata().getUid())); + operator.create(ConfigMap.class, relatedConfigMap(customResource.getMetadata().getName())); waitForCRStatusValue(INITIAL_STATUS_MESSAGE); configMap.getData().put(TARGET_CONFIG_MAP_KEY, UPDATE_STATUS_MESSAGE); - operator.replace(ConfigMap.class, configMap); + configMap = operator.replace(ConfigMap.class, configMap); waitForCRStatusValue(UPDATE_STATUS_MESSAGE); } @@ -51,7 +51,7 @@ private ConfigMap relatedConfigMap(String relatedResourceAnnotation) { ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setName(RESOURCE_NAME); objectMeta.setAnnotations(new HashMap<>()); - objectMeta.getAnnotations().put(RELATED_RESOURCE_UID, relatedResourceAnnotation); + objectMeta.getAnnotations().put(RELATED_RESOURCE_NAME, relatedResourceAnnotation); configMap.setMetadata(objectMeta); configMap.setData(new HashMap<>()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index a0b7805ccf..322ff6194e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -29,7 +29,7 @@ public class InformerEventSourceTestCustomReconciler implements private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomReconciler.class); - public static final String RELATED_RESOURCE_UID = "relatedResourceName"; + public static final String RELATED_RESOURCE_NAME = "relatedResourceName"; public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; private KubernetesClient kubernetesClient; @@ -38,7 +38,7 @@ public class InformerEventSourceTestCustomReconciler implements @Override public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { eventSource = new InformerEventSource<>(kubernetesClient, ConfigMap.class, - Mappers.fromAnnotation(RELATED_RESOURCE_UID)); + Mappers.fromAnnotation(RELATED_RESOURCE_NAME)); eventSourceRegistry.registerEventSource(eventSource); } diff --git a/pom.xml b/pom.xml index b788e40919..c11942d447 100644 --- a/pom.xml +++ b/pom.xml @@ -76,8 +76,9 @@ operator-framework-core operator-framework-junit5 operator-framework - samples - micrometer-support + smoke-test-samples + micrometer-support + sample-operators @@ -271,6 +272,7 @@ **/*IT.java + **/*E2E.java @@ -316,6 +318,7 @@ **/*Test.java **/*IT.java + **/*E2E.java @@ -335,6 +338,27 @@ **/*Test.java + **/*E2E.java + + + + + + + + end-to-end-tests + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*E2E.java + + + **/*Test.java + **/*IT.java @@ -352,6 +376,7 @@ **/*IT.java + **/*E2E.java diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml new file mode 100644 index 0000000000..289a2889c3 --- /dev/null +++ b/sample-operators/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + io.javaoperatorsdk + java-operator-sdk + 2.0.0-SNAPSHOT + + + sample-operators + Operator SDK - Samples + pom + + + 3.1.4 + + + + tomcat-operator + + + + + io.javaoperatorsdk + operator-framework + 1.9.2 + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.13.3 + + + org.takes + takes + 1.19 + + + junit + junit + 4.13.1 + test + + + org.awaitility + awaitility + 4.1.0 + test + + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/distroless/java:11 + + + tomcat-operator + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + + \ No newline at end of file diff --git a/sample-operators/tomcat-operator/README.md b/sample-operators/tomcat-operator/README.md new file mode 100644 index 0000000000..c16335ae80 --- /dev/null +++ b/sample-operators/tomcat-operator/README.md @@ -0,0 +1,76 @@ +# Tomcat Operator + +Creates a Tomcat deployment from a Custom Resource, while keeping the WAR separated with another Custom Resource. + +This sample demonstrates the following capabilities of the Java Operator SDK: +* Multiple Controllers in a single Operator. The Tomcat resource is managed by the TomcatController while the Webapp +resource is managed by the WebappController. +* Reacting to events about resources created by the controller. The TomcatController will receive events about the +Deployment resources it created. See EventSource section below for more detail. + +## Example input for creating a Tomcat instance +``` +apiVersion: "tomcatoperator.io/v1" +kind: Tomcat +metadata: + name: test-tomcat1 +spec: + version: 9.0 + replicas: 2 +``` + +## Example input for the Webapp +``` +apiVersion: "tomcatoperator.io/v1" +kind: Webapp +metadata: + name: sample-webapp1 +spec: + tomcat: test-tomcat1 + url: http://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war + contextPath: mysample +``` + +## Getting started / Testing + +The quickest way to try the operator is to run it on your local machine, while it connects to a +local or remote Kubernetes cluster. When you start it, it will use the current kubectl context on +your machine to connect to the cluster. + +Before you run it you have to install the CRDs on your cluster by running: +- `kubectl apply -f target/classes/META-INF/fabric8/tomcats.tomcatoperator.io-v1.yml` +- `kubectl apply -f target/classes/META-INF/fabric8/webapps.tomcatoperator.io-v1.yml` + +The CRDs are generated automatically from your code by simply adding the `crd-generator-apt` +dependency to your `pom.xml` file. + +When the Operator is running you can create some Tomcat Custom Resources. You can find a sample +custom resources in the k8s folder. + +If you want the Operator to be running as a deployment in your cluster, follow the below steps. + +## Build + +You can build the sample using `mvn install jib:dockerBuild` this will produce a Docker image you +can push to the registry of your choice. The JAR file is built using your local Maven and JDK and +then copied into the Docker image. + +## Install Operator into cluster + +Install the CRDs as shown above if you haven't already, then +run `kubectl apply -f k8s/operator.yaml`. Now you can create Tomcat instances with CRs (see examples +above). + +## EventSources +The TomcatController is listening to events about Deployments created by the TomcatOperator by registering a +InformerEventSource with the EventSourceManager. The InformerEventSource will in turn register a watch on +all Deployments managed by the Controller (identified by the `app.kubernetes.io/managed-by` label). +When an event from a Deployment is received we have to identify which Tomcat object does the Deployment +belong to. This is done when the InformerEventSource creates the event. + +The TomcatController has to take care of setting the `app.kubernetes.io/managed-by` label on the Deployment so the +InformerEventSource can watch the right Deployments. +The TomcatController also has to set `ownerReference` on the Deployment so later the InformerEventSource can +identify which Tomcat does the Deployment belong to. This is necessary so the frameowork can call the Controller +`createOrUpdate` method correctly. + diff --git a/sample-operators/tomcat-operator/k8s/operator.yaml b/sample-operators/tomcat-operator/k8s/operator.yaml new file mode 100644 index 0000000000..a88b6514e0 --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/operator.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: tomcat-operator + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: tomcat-operator + namespace: tomcat-operator + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tomcat-operator + namespace: tomcat-operator +spec: + selector: + matchLabels: + app: tomcat-operator + template: + metadata: + labels: + app: tomcat-operator + spec: + serviceAccountName: tomcat-operator + containers: + - name: operator + image: tomcat-operator + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 1 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: tomcat-operator-admin +subjects: +- kind: ServiceAccount + name: tomcat-operator + namespace: tomcat-operator +roleRef: + kind: ClusterRole + name: tomcat-operator + apiGroup: "" + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: tomcat-operator +rules: +- apiGroups: + - "" + - "extensions" + - "apps" + resources: + - deployments + - services + - pods + - pods/exec + verbs: + - '*' +- apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - '*' +- apiGroups: + - "tomcatoperator.io" + resources: + - tomcats + - tomcats/status + - webapps + - webapps/status + verbs: + - '*' \ No newline at end of file diff --git a/sample-operators/tomcat-operator/k8s/tomcat-sample1.yaml b/sample-operators/tomcat-operator/k8s/tomcat-sample1.yaml new file mode 100644 index 0000000000..ddd30663cd --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/tomcat-sample1.yaml @@ -0,0 +1,7 @@ +apiVersion: "tomcatoperator.io/v1" +kind: Tomcat +metadata: + name: test-tomcat1 +spec: + version: 9.0 + replicas: 2 diff --git a/sample-operators/tomcat-operator/k8s/tomcat-sample2.yaml b/sample-operators/tomcat-operator/k8s/tomcat-sample2.yaml new file mode 100644 index 0000000000..2dec431734 --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/tomcat-sample2.yaml @@ -0,0 +1,7 @@ +apiVersion: "tomcatoperator.io/v1" +kind: Tomcat +metadata: + name: test-tomcat2 +spec: + version: 8.0 + replicas: 4 diff --git a/sample-operators/tomcat-operator/k8s/webapp-sample1.yaml b/sample-operators/tomcat-operator/k8s/webapp-sample1.yaml new file mode 100644 index 0000000000..4913eb2444 --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/webapp-sample1.yaml @@ -0,0 +1,8 @@ +apiVersion: "tomcatoperator.io/v1" +kind: Webapp +metadata: + name: sample-webapp1 +spec: + tomcat: test-tomcat1 + url: http://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war + contextPath: mysample diff --git a/sample-operators/tomcat-operator/k8s/webapp-sample2.yaml b/sample-operators/tomcat-operator/k8s/webapp-sample2.yaml new file mode 100644 index 0000000000..e0415f9ce5 --- /dev/null +++ b/sample-operators/tomcat-operator/k8s/webapp-sample2.yaml @@ -0,0 +1,8 @@ +apiVersion: "tomcatoperator.io/v1" +kind: Webapp +metadata: + name: sample-webapp2 +spec: + tomcat: test-tomcat2 + url: charlottemach.com/assets/jax.war + contextPath: othercontext diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml new file mode 100644 index 0000000000..77f87b8158 --- /dev/null +++ b/sample-operators/tomcat-operator/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + io.javaoperatorsdk + sample-operators + 2.0.0-SNAPSHOT + + + sample-tomcat-operator + Operator SDK - Samples - Tomcat + Provisions Tomcat Pods and deploys Webapplications in them + jar + + + 11 + 11 + 3.1.4 + + + + + io.javaoperatorsdk + operator-framework + ${project.version} + + + io.fabric8 + crd-generator-apt + provided + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.13.3 + + + org.takes + takes + 1.19 + + + junit + junit + 4.13.1 + test + + + org.awaitility + awaitility + 4.1.0 + test + + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/distroless/java:11 + + + tomcat-operator + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + + \ No newline at end of file diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Tomcat.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Tomcat.java new file mode 100644 index 0000000000..7f60bd00d5 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Tomcat.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.sample; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +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("tomcatoperator.io") +@Version("v1") +@ShortNames("tc") +public class Tomcat extends CustomResource implements Namespaced { + + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); + } +} 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 new file mode 100644 index 0000000000..a7a7ca40ff --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; + +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.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; + +public class TomcatOperator { + + private static final Logger log = LoggerFactory.getLogger(TomcatOperator.class); + + public static void main(String[] args) throws IOException { + + Config config = new ConfigBuilder().withNamespace(null).build(); + KubernetesClient client = new DefaultKubernetesClient(config); + Operator operator = new Operator(client, DefaultConfigurationService.instance()); + operator.register(new TomcatReconciler(client)); + operator.register(new WebappReconciler(client)); + operator.start(); + + new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java new file mode 100644 index 0000000000..1aa7f1cfd0 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -0,0 +1,201 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.OwnerReference; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.RollableScalableResource; +import io.fabric8.kubernetes.client.dsl.ServiceResource; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; + +import static java.util.Collections.EMPTY_SET; + +/** + * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also + * creates a Service over which the Pods can be accessed. + */ +@ControllerConfiguration +public class TomcatReconciler implements Reconciler, EventSourceInitializer { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final KubernetesClient kubernetesClient; + + private volatile InformerEventSource informerEventSource; + + public TomcatReconciler(KubernetesClient client) { + this.kubernetesClient = client; + } + + @Override + public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + SharedIndexInformer deploymentInformer = + kubernetesClient.apps().deployments().inAnyNamespace() + .withLabel("app.kubernetes.io/managed-by", "tomcat-operator") + .runnableInformer(0); + + this.informerEventSource = new InformerEventSource<>(deploymentInformer, d -> { + var ownerReferences = d.getMetadata().getOwnerReferences(); + if (!ownerReferences.isEmpty()) { + return Set.of(new CustomResourceID(ownerReferences.get(0).getName(), + d.getMetadata().getNamespace())); + } else { + return EMPTY_SET; + } + }); + eventSourceRegistry.registerEventSource(this.informerEventSource); + } + + @Override + public UpdateControl reconcile(Tomcat tomcat, Context context) { + createOrUpdateDeployment(tomcat); + createOrUpdateService(tomcat); + + Deployment deployment = informerEventSource.getAssociated(tomcat); + + if (deployment != null) { + Tomcat updatedTomcat = + updateTomcatStatus(tomcat, deployment); + log.info( + "Updating status of Tomcat {} in namespace {} to {} ready replicas", + tomcat.getMetadata().getName(), + tomcat.getMetadata().getNamespace(), + tomcat.getStatus().getReadyReplicas()); + return UpdateControl.updateStatusSubResource(updatedTomcat); + } + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(Tomcat tomcat, Context context) { + deleteDeployment(tomcat); + deleteService(tomcat); + return DeleteControl.defaultDelete(); + } + + private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { + DeploymentStatus deploymentStatus = + Objects.requireNonNullElse(deployment.getStatus(), new DeploymentStatus()); + int readyReplicas = Objects.requireNonNullElse(deploymentStatus.getReadyReplicas(), 0); + TomcatStatus status = new TomcatStatus(); + status.setReadyReplicas(readyReplicas); + tomcat.setStatus(status); + return tomcat; + } + + private void createOrUpdateDeployment(Tomcat tomcat) { + String ns = tomcat.getMetadata().getNamespace(); + Deployment existingDeployment = + kubernetesClient + .apps() + .deployments() + .inNamespace(ns) + .withName(tomcat.getMetadata().getName()) + .get(); + if (existingDeployment == null) { + Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + deployment.getMetadata().setName(tomcat.getMetadata().getName()); + deployment.getMetadata().setNamespace(ns); + deployment.getMetadata().getLabels().put("app.kubernetes.io/part-of", + tomcat.getMetadata().getName()); + deployment.getMetadata().getLabels().put("app.kubernetes.io/managed-by", "tomcat-operator"); + // set tomcat version + deployment + .getSpec() + .getTemplate() + .getSpec() + .getContainers() + .get(0) + .setImage("tomcat:" + tomcat.getSpec().getVersion()); + deployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); + + // make sure label selector matches label (which has to be matched by service selector too) + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", tomcat.getMetadata().getName()); + deployment + .getSpec() + .getSelector() + .getMatchLabels() + .put("app", tomcat.getMetadata().getName()); + + OwnerReference ownerReference = deployment.getMetadata().getOwnerReferences().get(0); + ownerReference.setName(tomcat.getMetadata().getName()); + ownerReference.setUid(tomcat.getMetadata().getUid()); + + log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); + kubernetesClient.apps().deployments().inNamespace(ns).create(deployment); + } else { + existingDeployment + .getSpec() + .getTemplate() + .getSpec() + .getContainers() + .get(0) + .setImage("tomcat:" + tomcat.getSpec().getVersion()); + existingDeployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); + kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(existingDeployment); + } + } + + private void deleteDeployment(Tomcat tomcat) { + log.info("Deleting Deployment {}", tomcat.getMetadata().getName()); + RollableScalableResource deployment = + kubernetesClient + .apps() + .deployments() + .inNamespace(tomcat.getMetadata().getNamespace()) + .withName(tomcat.getMetadata().getName()); + if (deployment.get() != null) { + deployment.delete(); + } + } + + private void createOrUpdateService(Tomcat tomcat) { + Service service = loadYaml(Service.class, "service.yaml"); + service.getMetadata().setName(tomcat.getMetadata().getName()); + String ns = tomcat.getMetadata().getNamespace(); + service.getMetadata().setNamespace(ns); + service.getSpec().getSelector().put("app", tomcat.getMetadata().getName()); + log.info("Creating or updating Service {} in {}", service.getMetadata().getName(), ns); + kubernetesClient.services().inNamespace(ns).createOrReplace(service); + } + + private void deleteService(Tomcat tomcat) { + log.info("Deleting Service {}", tomcat.getMetadata().getName()); + ServiceResource service = + kubernetesClient + .services() + .inNamespace(tomcat.getMetadata().getNamespace()) + .withName(tomcat.getMetadata().getName()); + if (service.get() != null) { + service.delete(); + } + } + + private T loadYaml(Class clazz, String yaml) { + try (InputStream is = getClass().getResourceAsStream(yaml)) { + return Serialization.unmarshal(is, clazz); + } catch (IOException ex) { + throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); + } + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatSpec.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatSpec.java new file mode 100644 index 0000000000..fbd22f30f9 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatSpec.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample; + +public class TomcatSpec { + + private Integer version; + private Integer replicas; + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public Integer getReplicas() { + return replicas; + } + + public void setReplicas(Integer replicas) { + this.replicas = replicas; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatStatus.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatStatus.java new file mode 100644 index 0000000000..3bf3d2ab4b --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatStatus.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample; + +public class TomcatStatus { + + private Integer readyReplicas = 0; + + public Integer getReadyReplicas() { + return readyReplicas; + } + + public void setReadyReplicas(Integer readyReplicas) { + this.readyReplicas = readyReplicas; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java new file mode 100644 index 0000000000..d61f8791f7 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/Webapp.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +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.Version; + +/** + * Represents a web application deployed in a Tomcat deployment + */ +@Group("tomcatoperator.io") +@Version("v1") +public class Webapp extends CustomResource implements Namespaced { + + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java new file mode 100644 index 0000000000..64f5db8e77 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -0,0 +1,208 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.ExecListener; +import io.fabric8.kubernetes.client.dsl.ExecWatch; +import io.javaoperatorsdk.operator.api.*; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; + +import okhttp3.Response; + +@ControllerConfiguration +public class WebappReconciler implements Reconciler, EventSourceInitializer { + + private KubernetesClient kubernetesClient; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + public WebappReconciler(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + } + + @Override + public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + InformerEventSource tomcatEventSource = + new InformerEventSource<>(kubernetesClient, Tomcat.class, t -> { + // To create an event to a related WebApp resource and trigger the reconciliation + // we need to find which WebApp this Tomcat custom resource is related to. + // To find the related customResourceId of the WebApp resource we traverse the cache to + // and identify it based on naming convention. + var webAppInformer = + eventSourceRegistry.getCustomResourceEventSource() + .getInformer(CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY); + + var ids = webAppInformer.getStore().list().stream() + .filter( + (Webapp webApp) -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) + .map(webapp -> new CustomResourceID(webapp.getMetadata().getName(), + webapp.getMetadata().getNamespace())) + .collect(Collectors.toSet()); + return ids; + }); + eventSourceRegistry.registerEventSource(tomcatEventSource); + } + + /** + * This method will be called not only on changes to Webapp objects but also when Tomcat objects + * change. + */ + @Override + public UpdateControl reconcile(Webapp webapp, Context context) { + if (webapp.getStatus() != null + && Objects.equals(webapp.getSpec().getUrl(), webapp.getStatus().getDeployedArtifact())) { + return UpdateControl.noUpdate(); + } + + var tomcatClient = kubernetesClient.customResources(Tomcat.class); + Tomcat tomcat = tomcatClient.inNamespace(webapp.getMetadata().getNamespace()) + .withName(webapp.getSpec().getTomcat()).get(); + if (tomcat == null) { + throw new IllegalStateException("Cannot find Tomcat " + webapp.getSpec().getTomcat() + + " for Webapp " + webapp.getMetadata().getName() + " in namespace " + + webapp.getMetadata().getNamespace()); + } + + if (tomcat.getStatus() != null + && Objects.equals(tomcat.getSpec().getReplicas(), tomcat.getStatus().getReadyReplicas())) { + log.info( + "Tomcat is ready and webapps not yet deployed. Commencing deployment of {} in Tomcat {}", + webapp.getMetadata().getName(), tomcat.getMetadata().getName()); + String[] command = new String[] {"wget", "-O", + "/data/" + webapp.getSpec().getContextPath() + ".war", webapp.getSpec().getUrl()}; + if (log.isInfoEnabled()) { + command = new String[] {"time", "wget", "-O", + "/data/" + webapp.getSpec().getContextPath() + ".war", webapp.getSpec().getUrl()}; + } + + String[] commandStatusInAllPods = executeCommandInAllPods(kubernetesClient, webapp, command); + + if (webapp.getStatus() == null) { + webapp.setStatus(new WebappStatus()); + } + webapp.getStatus().setDeployedArtifact(webapp.getSpec().getUrl()); + webapp.getStatus().setDeploymentStatus(commandStatusInAllPods); + return UpdateControl.updateStatusSubResource(webapp); + } else { + log.info("WebappController invoked but Tomcat not ready yet ({}/{})", + tomcat.getStatus() != null ? tomcat.getStatus().getReadyReplicas() : 0, + tomcat.getSpec().getReplicas()); + return UpdateControl.noUpdate(); + } + } + + @Override + public DeleteControl cleanup(Webapp webapp, Context context) { + + String[] command = new String[] {"rm", "/data/" + webapp.getSpec().getContextPath() + ".war"}; + String[] commandStatusInAllPods = executeCommandInAllPods(kubernetesClient, webapp, command); + if (webapp.getStatus() != null) { + webapp.getStatus().setDeployedArtifact(null); + webapp.getStatus().setDeploymentStatus(commandStatusInAllPods); + } + return DeleteControl.defaultDelete(); + } + + private String[] executeCommandInAllPods( + KubernetesClient kubernetesClient, Webapp webapp, String[] command) { + String[] status = new String[0]; + + Deployment deployment = + kubernetesClient + .apps() + .deployments() + .inNamespace(webapp.getMetadata().getNamespace()) + .withName(webapp.getSpec().getTomcat()) + .get(); + + if (deployment != null) { + List pods = + kubernetesClient + .pods() + .inNamespace(webapp.getMetadata().getNamespace()) + .withLabels(deployment.getSpec().getSelector().getMatchLabels()) + .list() + .getItems(); + status = new String[pods.size()]; + for (int i = 0; i < pods.size(); i++) { + Pod pod = pods.get(i); + log.info( + "Executing command {} in Pod {}", + String.join(" ", command), + pod.getMetadata().getName()); + + CompletableFuture data = new CompletableFuture<>(); + try (ExecWatch execWatch = execCmd(pod, data, command)) { + status[i] = "" + pod.getMetadata().getName() + ":" + data.get(30, TimeUnit.SECONDS);; + } catch (ExecutionException e) { + status[i] = "" + pod.getMetadata().getName() + ": ExecutionException - " + e.getMessage(); + } catch (InterruptedException e) { + status[i] = + "" + pod.getMetadata().getName() + ": InterruptedException - " + e.getMessage(); + } catch (TimeoutException e) { + status[i] = "" + pod.getMetadata().getName() + ": TimeoutException - " + e.getMessage(); + } + } + } + return status; + } + + private ExecWatch execCmd(Pod pod, CompletableFuture data, String... command) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + return kubernetesClient.pods() + .inNamespace(pod.getMetadata().getNamespace()) + .withName(pod.getMetadata().getName()) + .inContainer("war-downloader") + .writingOutput(baos) + .writingError(baos) + .usingListener(new SimpleListener(data, baos)) + .exec(command); + } + + static class SimpleListener implements ExecListener { + + private CompletableFuture data; + private ByteArrayOutputStream baos; + private final Logger log = LoggerFactory.getLogger(getClass()); + + public SimpleListener(CompletableFuture data, ByteArrayOutputStream baos) { + this.data = data; + this.baos = baos; + } + + @Override + public void onOpen(Response response) { + log.debug("Reading data... " + response.message()); + } + + @Override + public void onFailure(Throwable t, Response response) { + log.debug(t.getMessage() + " " + response.message()); + data.completeExceptionally(t); + } + + @Override + public void onClose(int code, String reason) { + log.debug("Exit with: " + code + " and with reason: " + reason); + data.complete(baos.toString()); + } + } + +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappSpec.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappSpec.java new file mode 100644 index 0000000000..a34621c35b --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappSpec.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample; + +public class WebappSpec { + + private String url; + + private String contextPath; + + private String tomcat; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getContextPath() { + return contextPath; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + public String getTomcat() { + return tomcat; + } + + public void setTomcat(String tomcat) { + this.tomcat = tomcat; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java new file mode 100644 index 0000000000..8267abe24c --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappStatus.java @@ -0,0 +1,24 @@ +package io.javaoperatorsdk.operator.sample; + +public class WebappStatus { + + private String deployedArtifact; + + public String getDeployedArtifact() { + return deployedArtifact; + } + + public void setDeployedArtifact(String deployedArtifact) { + this.deployedArtifact = deployedArtifact; + } + + private String[] deploymentStatus; + + public String[] getDeploymentStatus() { + return deploymentStatus; + } + + public void setDeploymentStatus(String[] deploymentStatus) { + this.deploymentStatus = deploymentStatus; + } +} diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml new file mode 100644 index 0000000000..2f6f373c6c --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "" + labels: + app.kubernetes.io/part-of: "" + app.kubernetes.io/managed-by: "" # used for filtering of Deployments created by the controller + ownerReferences: # used for finding which Tomcat does this Deployment belong to + - apiVersion: apps/v1 + kind: Tomcat + name: "" + uid: "" +spec: + selector: + matchLabels: + app: "" + replicas: 1 + template: + metadata: + labels: + app: "" + spec: + containers: + - name: tomcat + image: tomcat:8.0 + ports: + - containerPort: 8080 + volumeMounts: + - mountPath: /usr/local/tomcat/webapps + name: webapps-volume + - name: war-downloader + image: busybox:1.28 + command: ['tail', '-f', '/dev/null'] + volumeMounts: + - name: webapps-volume + mountPath: /data + volumes: + - name: webapps-volume + emptydir: {} diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml new file mode 100644 index 0000000000..ab198643ed --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: "" +spec: + selector: + app: "" + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: NodePort diff --git a/sample-operators/tomcat-operator/src/main/resources/log4j2.xml b/sample-operators/tomcat-operator/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..a99aaf31b6 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java new file mode 100644 index 0000000000..f3cd473294 --- /dev/null +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -0,0 +1,136 @@ +package io.javaoperatorsdk.operator.sample; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.client.*; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; + +import static java.util.concurrent.TimeUnit.*; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; + +public class TomcatOperatorE2E { + + final static String TEST_NS = "tomcat-test"; + + final static Logger log = LoggerFactory.getLogger(TomcatOperatorE2E.class); + + @Test + public void test() { + Config config = new ConfigBuilder().withNamespace(null).build(); + KubernetesClient client = new DefaultKubernetesClient(config); + + // Use this if you want to run the test without deploying the Operator to Kubernetes + if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) { + Operator operator = new Operator(client, DefaultConfigurationService.instance()); + operator.register(new TomcatReconciler(client)); + operator.register(new WebappReconciler(client)); + operator.start(); + } + + Tomcat tomcat = new Tomcat(); + tomcat.setMetadata(new ObjectMetaBuilder() + .withName("test-tomcat1") + .withNamespace(TEST_NS) + .build()); + tomcat.setSpec(new TomcatSpec()); + tomcat.getSpec().setReplicas(3); + tomcat.getSpec().setVersion(9); + + Webapp webapp1 = new Webapp(); + webapp1.setMetadata(new ObjectMetaBuilder() + .withName("test-webapp1") + .withNamespace(TEST_NS) + .build()); + webapp1.setSpec(new WebappSpec()); + webapp1.getSpec().setContextPath("webapp1"); + webapp1.getSpec().setTomcat(tomcat.getMetadata().getName()); + webapp1.getSpec().setUrl("http://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war"); + + var tomcatClient = client.customResources(Tomcat.class); + var webappClient = client.customResources(Webapp.class); + + Namespace testNs = new NamespaceBuilder().withMetadata( + new ObjectMetaBuilder().withName(TEST_NS).build()).build(); + + if (testNs != null) { + // We perform a pre-run cleanup instead of a post-run cleanup. This is to help with debugging + // test results when running against a persistent cluster. The test namespace would stay + // after the test run so we can check what's there, but it would be cleaned up during the next + // test run. + log.info("Cleanup: deleting test namespace {}", TEST_NS); + client.namespaces().delete(testNs); + await().atMost(5, MINUTES) + .until(() -> client.namespaces().withName("tomcat-test").get() == null); + } + + log.info("Creating test namespace {}", TEST_NS); + client.namespaces().create(testNs); + + log.info("Creating test Tomcat object: {}", tomcat); + tomcatClient.inNamespace(TEST_NS).create(tomcat); + log.info("Creating test Webapp object: {}", webapp1); + webappClient.inNamespace(TEST_NS).create(webapp1); + + log.info("Waiting 5 minutes for Tomcat and Webapp CR statuses to be updated"); + await().atMost(5, MINUTES).untilAsserted(() -> { + Tomcat updatedTomcat = + tomcatClient.inNamespace(TEST_NS).withName(tomcat.getMetadata().getName()).get(); + Webapp updatedWebapp = + webappClient.inNamespace(TEST_NS).withName(webapp1.getMetadata().getName()).get(); + assertThat(updatedTomcat.getStatus(), is(notNullValue())); + assertThat(updatedTomcat.getStatus().getReadyReplicas(), equalTo(3)); + assertThat(updatedWebapp.getStatus(), is(notNullValue())); + assertThat(updatedWebapp.getStatus().getDeployedArtifact(), is(notNullValue())); + }); + + String url = + "http://" + tomcat.getMetadata().getName() + "/" + webapp1.getSpec().getContextPath() + "/"; + log.info("Starting curl Pod and waiting 5 minutes for GET of {} to return 200", url); + + await("wait-for-webapp").atMost(6, MINUTES).untilAsserted(() -> { + try { + + log.info("Starting curl Pod to test if webapp was deployed correctly"); + Pod curlPod = client.run().inNamespace(TEST_NS) + .withRunConfig(new RunConfigBuilder() + .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) + .withName("curl") + .withImage("curlimages/curl:7.78.0") + .withRestartPolicy("Never") + .build()) + .done(); + log.info("Waiting for curl Pod to finish running"); + await("wait-for-curl-pod-run").atMost(2, MINUTES) + .until(() -> { + String phase = + client.pods().inNamespace(TEST_NS).withName("curl").get().getStatus().getPhase(); + return phase.equals("Succeeded") || phase.equals("Failed"); + }); + + String curlOutput = + client.pods().inNamespace(TEST_NS).withName(curlPod.getMetadata().getName()).getLog(); + log.info("Output from curl: '{}'", curlOutput); + assertThat(curlOutput, equalTo("200")); + } catch (KubernetesClientException ex) { + throw new AssertionError(ex); + } finally { + log.info("Deleting curl Pod"); + client.pods().inNamespace(TEST_NS).withName("curl").delete(); + await("wait-for-curl-pod-stop").atMost(1, MINUTES) + .until(() -> client.pods().inNamespace(TEST_NS).withName("curl").get() == null); + } + }); + } + +} diff --git a/sample-operators/tomcat-operator/src/test/resources/log4j2.xml b/sample-operators/tomcat-operator/src/test/resources/log4j2.xml new file mode 100644 index 0000000000..a99aaf31b6 --- /dev/null +++ b/sample-operators/tomcat-operator/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/samples/README.md b/samples/README.md deleted file mode 100644 index 3af900da4f..0000000000 --- a/samples/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This samples folder contains simple artificial samples used for testing the framework rather -than showing off its real-world usage. - -More realistic samples can be found here: https://github.com/java-operator-sdk/samples diff --git a/smoke-test-samples/README.md b/smoke-test-samples/README.md new file mode 100644 index 0000000000..12daaa70f6 --- /dev/null +++ b/smoke-test-samples/README.md @@ -0,0 +1,4 @@ +This samples folder contains simple artificial samples used for testing the framework rather than +showing off its real-world usage. + +More realistic samples can be found in the `sample-operators` directory. diff --git a/samples/common/crd/test_object.yaml b/smoke-test-samples/common/crd/test_object.yaml similarity index 100% rename from samples/common/crd/test_object.yaml rename to smoke-test-samples/common/crd/test_object.yaml diff --git a/samples/common/pom.xml b/smoke-test-samples/common/pom.xml similarity index 85% rename from samples/common/pom.xml rename to smoke-test-samples/common/pom.xml index 5e873dfe23..ed1884aecd 100644 --- a/samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -5,12 +5,12 @@ io.javaoperatorsdk - java-operator-sdk-samples + java-operator-sdk-smoke-test-samples 2.0.0-SNAPSHOT - operator-framework-samples-common - Operator SDK - Samples - Common Files + operator-framework-smoke-test-samples-common + Operator SDK - Smoke Test Samples - Common Files Files shared between some of the samples jar diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java similarity index 100% rename from samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java rename to smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomService.java diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java similarity index 100% rename from samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java rename to smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java diff --git a/samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java similarity index 100% rename from samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java rename to smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/ServiceSpec.java diff --git a/samples/common/src/main/resources/log4j2.xml b/smoke-test-samples/common/src/main/resources/log4j2.xml similarity index 100% rename from samples/common/src/main/resources/log4j2.xml rename to smoke-test-samples/common/src/main/resources/log4j2.xml diff --git a/samples/pom.xml b/smoke-test-samples/pom.xml similarity index 85% rename from samples/pom.xml rename to smoke-test-samples/pom.xml index 386cc0c160..566fbd98be 100644 --- a/samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -9,9 +9,9 @@ 2.0.0-SNAPSHOT - java-operator-sdk-samples - Operator SDK - Samples - Sample usage of the operator sdk + java-operator-sdk-smoke-test-samples + Operator SDK - Smoke Test Samples + Samples to manually smoke the sdk pom diff --git a/samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml similarity index 70% rename from samples/pure-java/pom.xml rename to smoke-test-samples/pure-java/pom.xml index 74a9cebd56..d1a2de4c2b 100644 --- a/samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -5,19 +5,19 @@ io.javaoperatorsdk - java-operator-sdk-samples + java-operator-sdk-smoke-test-samples 2.0.0-SNAPSHOT - operator-framework-samples-pure-java - Operator SDK - Samples - Pure Java + operator-framework-smoke-test-samples-pure-java + Operator SDK - Smoke Test Samples - Pure Java Sample usage with pure java app jar io.javaoperatorsdk - operator-framework-samples-common + operator-framework-smoke-test-samples-common ${project.version} diff --git a/samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java similarity index 100% rename from samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java rename to smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java diff --git a/samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml similarity index 88% rename from samples/spring-boot-plain/pom.xml rename to smoke-test-samples/spring-boot-plain/pom.xml index 82fd73013b..de64a684e5 100644 --- a/samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -5,19 +5,19 @@ io.javaoperatorsdk - java-operator-sdk-samples + java-operator-sdk-smoke-test-samples 2.0.0-SNAPSHOT - operator-framework-samples-spring-boot-plain - Operator SDK - Samples - Spring Boot - Plain + operator-framework-smoke-test-samples-spring-boot + Operator SDK - Smoke Test Samples - Spring Boot Sample usage with Spring Boot jar io.javaoperatorsdk - operator-framework-samples-common + operator-framework-smoke-test-samples-common ${project.version} diff --git a/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java b/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java similarity index 100% rename from samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java rename to smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java diff --git a/samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java b/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java similarity index 100% rename from samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java rename to smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/SpringBootStarterSampleApplication.java diff --git a/samples/spring-boot-plain/src/main/resources/application.yaml b/smoke-test-samples/spring-boot-plain/src/main/resources/application.yaml similarity index 100% rename from samples/spring-boot-plain/src/main/resources/application.yaml rename to smoke-test-samples/spring-boot-plain/src/main/resources/application.yaml From 3d1f4a09c602426a97f3196ad9506bbfa4051970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 19 Nov 2021 16:23:02 +0100 Subject: [PATCH 0135/1608] ErrorStatusHandler - support for better error reporting in status (#685) --- docs/documentation/features.md | 24 +++++ .../operator/api/config/Cloner.java | 2 +- .../api/config/ConfigurationService.java | 5 +- .../api/reconciler/ErrorStatusHandler.java | 27 ++++++ .../processing/ReconciliationDispatcher.java | 88 +++++++++++++++---- .../internal/CustomResourceEventSource.java | 2 +- .../ReconciliationDispatcherTest.java | 87 +++++++++++------- .../operator/ErrorStatusHandlerIT.java | 58 ++++++++++++ .../operator/EventSourceIT.java | 1 - .../ErrorStatusHandlerTestCustomResource.java | 17 ++++ ...StatusHandlerTestCustomResourceStatus.java | 15 ++++ .../ErrorStatusHandlerTestReconciler.java | 53 +++++++++++ 12 files changed, 323 insertions(+), 56 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index ddaee104c9..9c4f30ff84 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -127,6 +127,30 @@ won't be a result of a retry, but the new event. A successful execution resets the retry. +### Setting Error Status After Last Retry Attempt + +In order to facilitate error reporting in case a last retry attempt fails, Reconciler can implement the following +interface: + +```java +public interface ErrorStatusHandler> { + + T updateErrorStatus(T resource, RuntimeException e); + +} +``` + +The `updateErrorStatus` resource is called when it's the last retry attempt according the retry configuration and the +reconciler execution still resulted in a runtime exception. + +The result of the method call is used to make a status sub-resource update on the custom resource. This is always a +sub-resource update request, so no update on custom resource itself (like spec of metadata) happens. Note that this +update request will also produce an event, and will result in a reconciliation if the controller is not generation +aware. + +Note that the scope of this feature is only the `reconcile` method of the reconciler, since there should not be updates +on custom resource after it is marked for deletion. + ### Correctness and Automatic Retries There is a possibility to turn of the automatic retries. This is not desirable, unless there is a very specific reason. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java index 92f569f36a..e50931a9b0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java @@ -4,6 +4,6 @@ public interface Cloner { - CustomResource clone(CustomResource object); + > T clone(T object); } 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 24e0605955..35d77844b8 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 @@ -19,9 +19,10 @@ public interface ConfigurationService { private final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Override - public CustomResource clone(CustomResource object) { + public > T clone(T object) { try { - return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(object), object.getClass()); + return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(object), + (Class) object.getClass()); } catch (JsonProcessingException e) { throw new IllegalStateException(e); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java new file mode 100644 index 0000000000..3ea7bb4862 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java @@ -0,0 +1,27 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import io.fabric8.kubernetes.client.CustomResource; + +public interface ErrorStatusHandler> { + + /** + *

+ * Reconcile can implement this interface in order to update the status sub-resource in the case + * when the last reconciliation retry attempt is failed on the Reconciler. In that case the + * updateErrorStatus is called automatically. + *

+ * The result of the method call is used to make a status sub-resource update on the custom + * resource. This is always a sub-resource update request, so no update on custom resource itself + * (like spec of metadata) happens. Note that this update request will also produce an event, and + * will result in a reconciliation if the controller is not generation aware. + *

+ * Note that the scope of this feature is only the reconcile method of the reconciler, since there + * should not be updates on custom resource after it is marked for deletion. + * + * @param resource to update the status on + * @param e exception thrown from the reconciler + * @return the updated resource + */ + T updateErrorStatus(T resource, RuntimeException e); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java index fa01bce577..b38b880442 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java @@ -14,6 +14,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -109,27 +110,76 @@ private PostExecutionControl handleCreateOrUpdate( updateCustomResourceWithFinalizer(resource); return PostExecutionControl.onlyFinalizerAdded(); } else { - log.debug( - "Executing createOrUpdate for resource {} with version: {} with execution scope: {}", - getName(resource), - getVersion(resource), - executionScope); + try { + var resourceForExecution = + cloneResourceForErrorStatusHandlerIfNeeded(resource, context); + return createOrUpdateExecution(executionScope, resourceForExecution, context); + } catch (RuntimeException e) { + handleLastAttemptErrorStatusHandler(resource, context, e); + throw e; + } + } + } + + /** + * Resource make sense only to clone for the ErrorStatusHandler. Otherwise, this operation can be + * skipped since it can be memory and time-consuming. However, it needs to be cloned since it's + * common that the custom resource is changed during an execution, and it's much cleaner to have + * to original resource in place for status update. + */ + private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { + if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context)) { + return controller.getConfiguration().getConfigurationService().getResourceCloner() + .clone(resource); + } else { + return resource; + } + } - UpdateControl updateControl = controller.reconcile(resource, context); - R updatedCustomResource = null; - if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { - updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); - updateControl - .getCustomResource() - .getMetadata() - .setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion()); - updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); - } else if (updateControl.isUpdateStatusSubResource()) { - updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); - } else if (updateControl.isUpdateCustomResource()) { - updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); + private PostExecutionControl createOrUpdateExecution(ExecutionScope executionScope, + R resource, Context context) { + log.debug( + "Executing createOrUpdate for resource {} with version: {} with execution scope: {}", + getName(resource), + getVersion(resource), + executionScope); + + UpdateControl updateControl = controller.reconcile(resource, context); + R updatedCustomResource = null; + if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { + updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); + updateControl + .getCustomResource() + .getMetadata() + .setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion()); + updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); + } else if (updateControl.isUpdateStatusSubResource()) { + updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); + } else if (updateControl.isUpdateCustomResource()) { + updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); + } + return createPostExecutionControl(updatedCustomResource, updateControl); + } + + private void handleLastAttemptErrorStatusHandler(R resource, Context context, + RuntimeException e) { + if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context)) { + try { + var updatedResource = ((ErrorStatusHandler) controller.getReconciler()) + .updateErrorStatus(resource, e); + customResourceFacade.updateStatus(updatedResource); + } catch (RuntimeException ex) { + log.error("Error during error status handling.", ex); } - return createPostExecutionControl(updatedCustomResource, updateControl); + } + } + + private boolean isLastAttemptOfRetryAndErrorStatusHandlerPresent(Context context) { + if (context.getRetryInfo().isPresent()) { + return context.getRetryInfo().get().isLastAttempt() + && controller.getReconciler() instanceof ErrorStatusHandler; + } else { + return false; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java index 67998c7584..a912072e5c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java @@ -172,7 +172,7 @@ public Optional getCustomResource(CustomResourceID resourceID) { if (resource == null) { return Optional.empty(); } else { - return Optional.of((T) (cloner.clone(resource))); + return Optional.of(cloner.clone(resource)); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java index 05b4b68ff4..781b48a53b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java @@ -13,11 +13,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.ReconciliationDispatcher.CustomResourceFacade; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -27,20 +23,16 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class ReconciliationDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; + public static final String ERROR_MESSAGE = "ErrorMessage"; private TestCustomResource testCustomResource; private ReconciliationDispatcher reconciliationDispatcher; - private final Reconciler controller = mock(Reconciler.class); + private final Reconciler reconciler = mock(Reconciler.class, + withSettings().extraInterfaces(ErrorStatusHandler.class)); private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); private final ConfigurationService configService = mock(ConfigurationService.class); @@ -51,7 +43,7 @@ class ReconciliationDispatcherTest { void setup() { testCustomResource = TestUtils.testCustomResource(); reconciliationDispatcher = - init(testCustomResource, controller, configuration, customResourceFacade); + init(testCustomResource, reconciler, configuration, customResourceFacade); } private > ReconciliationDispatcher init(R customResource, @@ -62,6 +54,7 @@ void setup() { when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configService.getMetrics()).thenReturn(Metrics.NOOP); when(configuration.getConfigurationService()).thenReturn(configService); + when(configService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); when(reconciler.reconcile(eq(customResource), any())) .thenReturn(UpdateControl.updateCustomResource(customResource)); when(reconciler.cleanup(eq(customResource), any())) @@ -77,7 +70,7 @@ void setup() { void addFinalizerOnNewResource() { assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, never()) + verify(reconciler, never()) .reconcile(ArgumentMatchers.eq(testCustomResource), any()); verify(customResourceFacade, times(1)) .replaceWithLock( @@ -89,7 +82,7 @@ void addFinalizerOnNewResource() { void callCreateOrUpdateOnNewResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, times(1)) + verify(reconciler, times(1)) .reconcile(ArgumentMatchers.eq(testCustomResource), any()); } @@ -97,7 +90,7 @@ void callCreateOrUpdateOnNewResourceIfFinalizerSet() { void updatesOnlyStatusSubResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.reconcile(eq(testCustomResource), any())) + when(reconciler.reconcile(eq(testCustomResource), any())) .thenReturn(UpdateControl.updateStatusSubResource(testCustomResource)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -110,7 +103,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { void updatesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.reconcile(eq(testCustomResource), any())) + when(reconciler.reconcile(eq(testCustomResource), any())) .thenReturn(UpdateControl.updateCustomResourceAndStatus(testCustomResource)); when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); @@ -125,7 +118,7 @@ void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, times(1)) + verify(reconciler, times(1)) .reconcile(ArgumentMatchers.eq(testCustomResource), any()); } @@ -137,7 +130,7 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, times(1)).cleanup(eq(testCustomResource), any()); + verify(reconciler, times(1)).cleanup(eq(testCustomResource), any()); } /** @@ -150,7 +143,7 @@ void callDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller).cleanup(eq(testCustomResource), any()); + verify(reconciler).cleanup(eq(testCustomResource), any()); } @Test @@ -159,7 +152,7 @@ void doNotCallDeleteIfMarkedForDeletionWhenFinalizerHasAlreadyBeenRemoved() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, never()).cleanup(eq(testCustomResource), any()); + verify(reconciler, never()).cleanup(eq(testCustomResource), any()); } private void configureToNotUseFinalizer() { @@ -170,7 +163,7 @@ private void configureToNotUseFinalizer() { when(configuration.getConfigurationService()).thenReturn(configService); when(configuration.useFinalizer()).thenReturn(false); reconciliationDispatcher = - new ReconciliationDispatcher(new Controller(controller, configuration, null), + new ReconciliationDispatcher(new Controller(reconciler, configuration, null), customResourceFacade); } @@ -198,7 +191,7 @@ void removesDefaultFinalizerOnDeleteIfSet() { void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.cleanup(eq(testCustomResource), any())) + when(reconciler.cleanup(eq(testCustomResource), any())) .thenReturn(DeleteControl.noFinalizerRemoval()); markForDeletion(testCustomResource); @@ -212,7 +205,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.reconcile(eq(testCustomResource), any())) + when(reconciler.reconcile(eq(testCustomResource), any())) .thenReturn(UpdateControl.noUpdate()); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -223,7 +216,7 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { @Test void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); - when(controller.reconcile(eq(testCustomResource), any())) + when(reconciler.reconcile(eq(testCustomResource), any())) .thenReturn(UpdateControl.noUpdate()); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -240,7 +233,7 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); - verify(controller, never()).cleanup(eq(testCustomResource), any()); + verify(reconciler, never()).cleanup(eq(testCustomResource), any()); } @Test @@ -249,7 +242,7 @@ void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(controller, times(2)).reconcile(eq(testCustomResource), any()); + verify(reconciler, times(2)).reconcile(eq(testCustomResource), any()); } @Test @@ -273,8 +266,8 @@ public boolean isLastAttempt() { ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(Context.class); - verify(controller, times(1)) - .reconcile(eq(testCustomResource), contextArgumentCaptor.capture()); + verify(reconciler, times(1)) + .reconcile(any(), contextArgumentCaptor.capture()); Context context = contextArgumentCaptor.getValue(); final var retryInfo = context.getRetryInfo().get(); assertThat(retryInfo.getAttemptCount()).isEqualTo(2); @@ -285,7 +278,7 @@ public boolean isLastAttempt() { void setReScheduleToPostExecutionControlFromUpdateControl() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(controller.reconcile(eq(testCustomResource), any())) + when(reconciler.reconcile(eq(testCustomResource), any())) .thenReturn( UpdateControl.updateStatusSubResource(testCustomResource).rescheduleAfter(1000L)); @@ -300,7 +293,7 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(controller.cleanup(eq(testCustomResource), any())) + when(reconciler.cleanup(eq(testCustomResource), any())) .thenReturn(DeleteControl.noFinalizerRemoval().rescheduleAfter(1000L)); PostExecutionControl control = @@ -331,6 +324,36 @@ void setObservedGenerationForStatusIfNeeded() { } @Test + void callErrorStatusHandlerIfImplemented() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + + when(reconciler.reconcile(any(), any())) + .thenThrow(new IllegalStateException("Error Status Test")); + when(((ErrorStatusHandler) reconciler).updateErrorStatus(any(), any())).then(a -> { + testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); + return testCustomResource; + }); + + reconciliationDispatcher.handleExecution( + new ExecutionScope( + testCustomResource, + new RetryInfo() { + @Override + public int getAttemptCount() { + return 2; + } + + @Override + public boolean isLastAttempt() { + return true; + } + })); + + verify(customResourceFacade, times(1)).updateStatus(testCustomResource); + verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), + any()); + } + private ObservedGenCustomResource createObservedGenCustomResource() { ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); observedGenCustomResource.setMetadata(new ObjectMeta()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java new file mode 100644 index 0000000000..01908757bc --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java @@ -0,0 +1,58 @@ +package io.javaoperatorsdk.operator; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.sample.errorstatushandler.ErrorStatusHandlerTestCustomResource; +import io.javaoperatorsdk.operator.sample.errorstatushandler.ErrorStatusHandlerTestReconciler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class ErrorStatusHandlerIT { + + ErrorStatusHandlerTestReconciler reconciler = new ErrorStatusHandlerTestReconciler(); + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(reconciler, new GenericRetry().setMaxAttempts(3).withLinearRetry()) + .build(); + + @Test + public void testErrorMessageSetEventually() { + ErrorStatusHandlerTestCustomResource resource = + operator.create(ErrorStatusHandlerTestCustomResource.class, createCustomResource()); + + await() + .atMost(15, TimeUnit.SECONDS) + .pollInterval(1L, TimeUnit.SECONDS) + .untilAsserted( + () -> { + ErrorStatusHandlerTestCustomResource res = + operator.get(ErrorStatusHandlerTestCustomResource.class, + resource.getMetadata().getName()); + assertThat(res.getStatus()).isNotNull(); + assertThat(res.getStatus().getMessage()) + .isEqualTo(ErrorStatusHandlerTestReconciler.ERROR_STATUS_MESSAGE); + }); + } + + public ErrorStatusHandlerTestCustomResource createCustomResource() { + ErrorStatusHandlerTestCustomResource resource = new ErrorStatusHandlerTestCustomResource(); + resource.setMetadata( + new ObjectMetaBuilder() + .withName("error-status-test") + .withNamespace(operator.getNamespace()) + .build()); + return resource; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index 509cb74d42..033bbf2b8b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -47,7 +47,6 @@ public EventSourceTestCustomResource createTestCustomResource(String id) { .withNamespace(operator.getNamespace()) .withFinalizers(EventSourceTestCustomReconciler.FINALIZER_NAME) .build()); - resource.setKind("Eventsourcesample"); resource.setSpec(new EventSourceTestCustomResourceSpec()); resource.getSpec().setValue(id); return resource; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResource.java new file mode 100644 index 0000000000..d6056ea486 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.errorstatushandler; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("ErrorStatusHandlerTestCustomResource") +@ShortNames("esh") +public class ErrorStatusHandlerTestCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResourceStatus.java new file mode 100644 index 0000000000..03fc517ecd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResourceStatus.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.errorstatushandler; + +public class ErrorStatusHandlerTestCustomResourceStatus { + + private String message; + + public String getMessage() { + return message; + } + + public ErrorStatusHandlerTestCustomResourceStatus setMessage(String message) { + this.message = message; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java new file mode 100644 index 0000000000..7ad8f20bae --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.sample.errorstatushandler; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration +public class ErrorStatusHandlerTestReconciler + implements Reconciler, TestExecutionInfoProvider, + ErrorStatusHandler { + + private static final Logger log = LoggerFactory.getLogger(ErrorStatusHandlerTestReconciler.class); + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + public static final String ERROR_STATUS_MESSAGE = "Error Retries Exceeded"; + + @Override + public UpdateControl reconcile( + ErrorStatusHandlerTestCustomResource resource, Context context) { + var number = numberOfExecutions.addAndGet(1); + var retryAttempt = -1; + if (context.getRetryInfo().isPresent()) { + retryAttempt = context.getRetryInfo().get().getAttemptCount(); + } + log.info("Number of execution: {} retry attempt: {} , resource: {}", number, retryAttempt, + resource); + throw new IllegalStateException(); + } + + private void ensureStatusExists(ErrorStatusHandlerTestCustomResource resource) { + ErrorStatusHandlerTestCustomResourceStatus status = resource.getStatus(); + if (status == null) { + status = new ErrorStatusHandlerTestCustomResourceStatus(); + resource.setStatus(status); + } + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + @Override + public ErrorStatusHandlerTestCustomResource updateErrorStatus( + ErrorStatusHandlerTestCustomResource resource, RuntimeException e) { + log.info("Setting status."); + ensureStatusExists(resource); + resource.getStatus().setMessage(ERROR_STATUS_MESSAGE); + return resource; + } +} From 63b8cd9db2bf0a0e722193a4c9ceb04065a46427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 22 Nov 2021 10:49:12 +0100 Subject: [PATCH 0136/1608] Support Standard Kuberentes Resources not just CustomResource (#678) --- docs/documentation/features.md | 7 ++ .../micrometer/MicrometerMetrics.java | 18 ++--- .../io/javaoperatorsdk/operator/Operator.java | 10 +-- .../config/AbstractConfigurationService.java | 14 ++-- .../operator/api/config/Cloner.java | 4 +- .../api/config/ConfigurationService.java | 8 +-- .../config/ConfigurationServiceOverrider.java | 4 +- .../api/config/ControllerConfiguration.java | 19 ++--- .../ControllerConfigurationOverrider.java | 16 ++--- .../DefaultControllerConfiguration.java | 32 ++++----- .../operator/api/monitoring/Metrics.java | 10 +-- .../reconciler/ControllerConfiguration.java | 6 +- .../api/reconciler/ErrorStatusHandler.java | 4 +- .../reconciler/EventSourceInitializer.java | 4 +- .../operator/api/reconciler/Reconciler.java | 4 +- .../api/reconciler/UpdateControl.java | 34 ++++----- .../operator/processing/Controller.java | 12 ++-- .../operator/processing/EventMarker.java | 44 ++++++------ .../operator/processing/EventProcessor.java | 58 +++++++-------- .../operator/processing/ExecutionScope.java | 26 +++---- .../processing/KubernetesResourceUtils.java | 12 ++-- .../operator/processing/MDCUtils.java | 28 ++++---- .../processing/PostExecutionControl.java | 12 ++-- .../processing/ReconciliationDispatcher.java | 31 ++++---- .../operator/processing/ResourceCache.java | 8 +-- .../operator/processing/event/Event.java | 6 +- .../processing/event/EventSource.java | 2 +- .../processing/event/EventSourceManager.java | 24 +++---- .../processing/event/EventSourceRegistry.java | 8 +-- ...{CustomResourceID.java => ResourceID.java} | 12 ++-- ...ava => ControllerResourceEventSource.java} | 36 +++++----- .../event/internal/InformerEventSource.java | 28 ++++---- .../processing/event/internal/Mappers.java | 14 ++-- .../OnceWhitelistEventFilterEventFilter.java | 21 +++--- ...mResourceEvent.java => ResourceEvent.java} | 10 +-- ...ntFilter.java => ResourceEventFilter.java} | 8 +-- ...Filters.java => ResourceEventFilters.java} | 68 ++++++++++-------- .../event/internal/TimerEventSource.java | 22 +++--- .../operator/ControllerManagerTest.java | 9 +-- .../javaoperatorsdk/operator/TestUtils.java | 6 +- .../config/ControllerConfigurationTest.java | 2 +- .../operator/processing/EventMarkerTest.java | 40 +++++------ .../processing/EventProcessorTest.java | 50 ++++++------- .../ReconciliationDispatcherTest.java | 9 +-- .../event/EventSourceManagerTest.java | 4 +- ...=> ControllerResourceEventSourceTest.java} | 38 +++++----- ...ceWhitelistEventFilterEventFilterTest.java | 6 +- ...Test.java => ResourceEventFilterTest.java} | 20 +++--- .../event/internal/TimerEventSourceTest.java | 6 +- .../sample/simple/TestCustomReconciler.java | 2 +- .../operator/junit/OperatorExtension.java | 6 +- .../runtime/AnnotationConfiguration.java | 26 +++---- .../config/runtime/ClassMappingProvider.java | 6 +- .../runtime/DefaultConfigurationService.java | 6 +- .../runtime/RuntimeControllerMetadata.java | 14 ++-- .../operator/InformerEventSourceIT.java | 2 +- .../KubernetesResourceStatusUpdateIT.java | 72 +++++++++++++++++++ .../DefaultConfigurationServiceTest.java | 6 +- .../deployment/DeploymentReconciler.java | 56 +++++++++++++++ .../ErrorStatusHandlerTestReconciler.java | 4 +- .../MultilevelReconciler.java | 2 +- .../ReconcilerImplemented2Interfaces.java | 2 +- ...rImplementedIntermediateAbstractClass.java | 2 +- .../operator/sample/TomcatReconciler.java | 5 +- .../operator/sample/WebappReconciler.java | 11 ++- .../sample/CustomServiceReconciler.java | 2 +- 66 files changed, 624 insertions(+), 474 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{CustomResourceID.java => ResourceID.java} (76%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/{CustomResourceEventSource.java => ControllerResourceEventSource.java} (85%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/{CustomResourceEvent.java => ResourceEvent.java} (61%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/{CustomResourceEventFilter.java => ResourceEventFilter.java} (89%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/{CustomResourceEventFilters.java => ResourceEventFilters.java} (67%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/{CustomResourceEventSourceTest.java => ControllerResourceEventSourceTest.java} (76%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/{CustomResourceEventFilterTest.java => ResourceEventFilterTest.java} (91%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 9c4f30ff84..d281fd1a22 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -110,6 +110,13 @@ mostly for the cases when there is a long waiting period after a delete operatio you might want to either schedule a timed event to make sure the `deleteResource` is executed again or use event sources get notified about the state changes of a deleted resource. +## Automatic Observed Generation Handling + +## Support for Well Known (non-custom) Kubernetes Resources + +A Controller can be registered for a non-custom resource, so well known Kubernetes resources like ( +Ingress,Deployment,...). Note that automatic observed generation handling is not supported for these resources. + ## Automatic Retries on Error When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. The 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 19f4f78bf7..ebe193b101 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 @@ -7,8 +7,8 @@ import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; 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.Timer; @@ -53,30 +53,30 @@ public void receivedEvent(Event event) { } @Override - public void cleanupDoneFor(CustomResourceID customResourceUid) { + public void cleanupDoneFor(ResourceID customResourceUid) { incrementCounter(customResourceUid, "events.delete"); } - public void reconcileCustomResource(CustomResourceID customResourceID, + public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) { - incrementCounter(customResourceID, RECONCILIATIONS + "started", + incrementCounter(resourceID, RECONCILIATIONS + "started", RECONCILIATIONS + "retries.number", "" + retryInfo.getAttemptCount(), RECONCILIATIONS + "retries.last", "" + retryInfo.isLastAttempt()); } @Override - public void finishedReconciliation(CustomResourceID customResourceID) { - incrementCounter(customResourceID, RECONCILIATIONS + "success"); + public void finishedReconciliation(ResourceID resourceID) { + incrementCounter(resourceID, RECONCILIATIONS + "success"); } - public void failedReconciliation(CustomResourceID customResourceID, RuntimeException exception) { + public void failedReconciliation(ResourceID resourceID, RuntimeException exception) { var cause = exception.getCause(); if (cause == null) { cause = exception; } else if (cause instanceof RuntimeException) { cause = cause.getCause() != null ? cause.getCause() : cause; } - incrementCounter(customResourceID, RECONCILIATIONS + "failed", "exception", + incrementCounter(resourceID, RECONCILIATIONS + "failed", "exception", cause.getClass().getSimpleName()); } @@ -84,7 +84,7 @@ public void failedReconciliation(CustomResourceID customResourceID, RuntimeExcep return registry.gaugeMapSize(PREFIX + name + ".size", Collections.emptyList(), map); } - private void incrementCounter(CustomResourceID id, String counterName, String... additionalTags) { + private void incrementCounter(ResourceID id, String counterName, String... additionalTags) { var tags = List.of( "name", id.getName(), "name", id.getName(), "namespace", id.getNamespace().orElse(""), 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 405f4b6927..0ec41a495c 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 @@ -9,7 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Version; @@ -114,7 +114,7 @@ public void close() { * @param the {@code CustomResource} type associated with the controller * @throws OperatorException if a problem occurred during the registration process */ - public > void register(Reconciler controller) + public void register(Reconciler controller) throws OperatorException { register(controller, null); } @@ -132,7 +132,7 @@ public void close() { * @param the {@code CustomResource} type associated with the controller * @throws OperatorException if a problem occurred during the registration process */ - public > void register( + public void register( Reconciler reconciler, ControllerConfiguration configuration) throws OperatorException { final var existing = configurationService.getConfigurationFor(reconciler); @@ -157,7 +157,7 @@ public void close() { log.info( "Registered Controller: '{}' for CRD: '{}' for namespace(s): {}", configuration.getName(), - configuration.getCustomResourceClass(), + configuration.getResourceClass(), watchedNS); } } @@ -195,7 +195,7 @@ public synchronized void stop() { public synchronized void add(Controller controller) { final var configuration = controller.getConfiguration(); - final var crdName = configuration.getCRDName(); + final var crdName = configuration.getResourceTypeName(); final var existing = controllers.get(crdName); if (existing != null) { throw new OperatorException("Cannot register controller '" + configuration.getName() diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index b2eae5bbc2..0dda134c11 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -5,7 +5,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -18,15 +18,15 @@ public AbstractConfigurationService(Version version) { this.version = version; } - protected > void register(ControllerConfiguration config) { + protected void register(ControllerConfiguration config) { put(config, true); } - protected > void replace(ControllerConfiguration config) { + protected void replace(ControllerConfiguration config) { put(config, false); } - private > void put( + private void put( ControllerConfiguration config, boolean failIfExisting) { final var name = config.getName(); if (failIfExisting) { @@ -39,7 +39,7 @@ public AbstractConfigurationService(Version version) { config.setConfigurationService(this); } - protected > void throwExceptionOnNameCollision( + protected void throwExceptionOnNameCollision( String newControllerClassName, ControllerConfiguration existing) { throw new IllegalArgumentException( "Controller name '" @@ -51,7 +51,7 @@ public AbstractConfigurationService(Version version) { } @Override - public > ControllerConfiguration getConfigurationFor( + public ControllerConfiguration getConfigurationFor( Reconciler controller) { final var key = keyFor(controller); final var configuration = configurations.get(key); @@ -73,7 +73,7 @@ private String getControllersNameMessage() { + "."; } - protected > String keyFor(Reconciler controller) { + protected String keyFor(Reconciler controller) { return ControllerUtils.getNameFor(controller); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java index e50931a9b0..30b2cee0e9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Cloner.java @@ -1,9 +1,9 @@ package io.javaoperatorsdk.operator.api.config; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; public interface Cloner { - > T clone(T object); + R clone(R object); } 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 35d77844b8..bb692e9f9b 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 @@ -4,6 +4,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.monitoring.Metrics; @@ -19,10 +20,9 @@ public interface ConfigurationService { private final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Override - public > T clone(T object) { + public HasMetadata clone(HasMetadata object) { try { - return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(object), - (Class) object.getClass()); + return OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(object), object.getClass()); } catch (JsonProcessingException e) { throw new IllegalStateException(e); } @@ -37,7 +37,7 @@ public interface ConfigurationService { * @return the {@link ControllerConfiguration} associated with the specified controller or {@code * null} if no configuration exists for the controller */ - > ControllerConfiguration getConfigurationFor( + ControllerConfiguration getConfigurationFor( Reconciler controller); /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 3c223cd23f..08adf6e864 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -2,8 +2,8 @@ import java.util.Set; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -61,7 +61,7 @@ public ConfigurationServiceOverrider withMetrics(Metrics metrics) { public ConfigurationService build() { return new ConfigurationService() { @Override - public > ControllerConfiguration getConfigurationFor( + public ControllerConfiguration getConfigurationFor( Reconciler controller) { return original.getConfigurationFor(controller); } 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 70187b104c..b729ce2d07 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 @@ -4,23 +4,24 @@ import java.util.Collections; import java.util.Set; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilters; -public interface ControllerConfiguration> { +public interface ControllerConfiguration { default String getName() { return ControllerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); } - default String getCRDName() { - return CustomResource.getCRDName(getCustomResourceClass()); + default String getResourceTypeName() { + return CustomResource.getCRDName(getResourceClass()); } default String getFinalizer() { - return ControllerUtils.getDefaultFinalizerName(getCRDName()); + return ControllerUtils.getDefaultFinalizerName(getResourceTypeName()); } /** @@ -39,7 +40,7 @@ default boolean isGenerationAware() { return true; } - default Class getCustomResourceClass() { + default Class getResourceClass() { ParameterizedType type = (ParameterizedType) getClass().getGenericInterfaces()[0]; return (Class) type.getActualTypeArguments()[0]; } @@ -110,7 +111,7 @@ default boolean useFinalizer() { * * @return filter */ - default CustomResourceEventFilter getEventFilter() { - return CustomResourceEventFilters.passthrough(); + default ResourceEventFilter getEventFilter() { + return ResourceEventFilters.passthrough(); } } 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 006d707803..31be7cb983 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 @@ -4,17 +4,17 @@ import java.util.List; import java.util.Set; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; -public class ControllerConfigurationOverrider> { +public class ControllerConfigurationOverrider { private String finalizer; private boolean generationAware; private final Set namespaces; private RetryConfiguration retry; private String labelSelector; - private CustomResourceEventFilter customResourcePredicate; + private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private ControllerConfigurationOverrider(ControllerConfiguration original) { @@ -69,7 +69,7 @@ public ControllerConfigurationOverrider withLabelSelector(String labelSelecto } public ControllerConfigurationOverrider withCustomResourcePredicate( - CustomResourceEventFilter customResourcePredicate) { + ResourceEventFilter customResourcePredicate) { this.customResourcePredicate = customResourcePredicate; return this; } @@ -78,18 +78,18 @@ public ControllerConfiguration build() { return new DefaultControllerConfiguration<>( original.getAssociatedReconcilerClassName(), original.getName(), - original.getCRDName(), + original.getResourceTypeName(), finalizer, generationAware, namespaces, retry, labelSelector, customResourcePredicate, - original.getCustomResourceClass(), + original.getResourceClass(), original.getConfigurationService()); } - public static > ControllerConfigurationOverrider override( + public static ControllerConfigurationOverrider override( ControllerConfiguration original) { return new ControllerConfigurationOverrider<>(original); } 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 index a0336eb92c..5e8f2611bc 100644 --- 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 @@ -3,10 +3,10 @@ import java.util.Collections; import java.util.Set; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; -public class DefaultControllerConfiguration> +public class DefaultControllerConfiguration implements ControllerConfiguration { private final String associatedControllerClassName; @@ -18,8 +18,8 @@ public class DefaultControllerConfiguration> private final boolean watchAllNamespaces; private final RetryConfiguration retryConfiguration; private final String labelSelector; - private final CustomResourceEventFilter customResourceEventFilter; - private final Class customResourceClass; + private final ResourceEventFilter resourceEventFilter; + private final Class resourceClass; private ConfigurationService service; public DefaultControllerConfiguration( @@ -31,8 +31,8 @@ public DefaultControllerConfiguration( Set namespaces, RetryConfiguration retryConfiguration, String labelSelector, - CustomResourceEventFilter customResourceEventFilter, - Class customResourceClass, + ResourceEventFilter resourceEventFilter, + Class resourceClass, ConfigurationService service) { this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -47,10 +47,10 @@ public DefaultControllerConfiguration( ? ControllerConfiguration.super.getRetryConfiguration() : retryConfiguration; this.labelSelector = labelSelector; - this.customResourceEventFilter = customResourceEventFilter; - this.customResourceClass = - customResourceClass == null ? ControllerConfiguration.super.getCustomResourceClass() - : customResourceClass; + this.resourceEventFilter = resourceEventFilter; + this.resourceClass = + resourceClass == null ? ControllerConfiguration.super.getResourceClass() + : resourceClass; setConfigurationService(service); } @@ -60,7 +60,7 @@ public String getName() { } @Override - public String getCRDName() { + public String getResourceTypeName() { return crdName; } @@ -114,12 +114,12 @@ public String getLabelSelector() { } @Override - public Class getCustomResourceClass() { - return customResourceClass; + public Class getResourceClass() { + return resourceClass; } @Override - public CustomResourceEventFilter getEventFilter() { - return customResourceEventFilter; + public ResourceEventFilter getEventFilter() { + return resourceEventFilter; } } 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 efb99cc0ae..25472c4626 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 @@ -3,23 +3,23 @@ import java.util.Map; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; public interface Metrics { Metrics NOOP = new Metrics() {}; default void receivedEvent(Event event) {} - default void reconcileCustomResource(CustomResourceID customResourceID, + default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) {} - default void failedReconciliation(CustomResourceID customResourceID, + default void failedReconciliation(ResourceID resourceID, RuntimeException exception) {} - default void cleanupDoneFor(CustomResourceID customResourceUid) {} + default void cleanupDoneFor(ResourceID customResourceUid) {} - default void finishedReconciliation(CustomResourceID resourceID) {} + default void finishedReconciliation(ResourceID resourceID) {} interface ControllerExecution { 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 199a4985f8..3a522c1347 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,7 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @@ -54,10 +54,10 @@ /** - * Optional list of classes providing custom {@link CustomResourceEventFilter}. + * Optional list of classes providing custom {@link ResourceEventFilter}. * * @return the list of event filters. */ @SuppressWarnings("rawtypes") - Class[] eventFilters() default {}; + Class[] eventFilters() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java index 3ea7bb4862..578d3ff8a6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java @@ -1,8 +1,8 @@ package io.javaoperatorsdk.operator.api.reconciler; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; -public interface ErrorStatusHandler> { +public interface ErrorStatusHandler { /** *

diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index f782b8bec7..b568814739 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -1,9 +1,9 @@ package io.javaoperatorsdk.operator.api.reconciler; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; -public interface EventSourceInitializer> { +public interface EventSourceInitializer { /** * In this typically you might want to register event sources. But can access diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 937969126f..7a6977d940 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -1,8 +1,8 @@ package io.javaoperatorsdk.operator.api.reconciler; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; -public interface Reconciler> { +public interface Reconciler { /** * Note that this method is used in combination of finalizers. If automatic finalizer handling is diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index 7d7b6b6e0b..3646a6df0f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -1,29 +1,29 @@ package io.javaoperatorsdk.operator.api.reconciler; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; @SuppressWarnings("rawtypes") -public class UpdateControl extends BaseControl> { +public class UpdateControl extends BaseControl> { - private final T customResource; + private final T resource; private final boolean updateStatusSubResource; - private final boolean updateCustomResource; + private final boolean updateResource; private UpdateControl( - T customResource, boolean updateStatusSubResource, boolean updateCustomResource) { - if ((updateCustomResource || updateStatusSubResource) && customResource == null) { + T resource, boolean updateStatusSubResource, boolean updateResource) { + if ((updateResource || updateStatusSubResource) && resource == null) { throw new IllegalArgumentException("CustomResource cannot be null in case of update"); } - this.customResource = customResource; + this.resource = resource; this.updateStatusSubResource = updateStatusSubResource; - this.updateCustomResource = updateCustomResource; + this.updateResource = updateResource; } - public static UpdateControl updateCustomResource(T customResource) { + public static UpdateControl updateResource(T customResource) { return new UpdateControl<>(customResource, false, true); } - public static UpdateControl updateStatusSubResource( + public static UpdateControl updateStatusSubResource( T customResource) { return new UpdateControl<>(customResource, true, false); } @@ -35,28 +35,28 @@ public static UpdateControl updateStatusSubResourc * @param customResource - custom resource to use in both API calls * @return UpdateControl instance */ - public static > UpdateControl updateCustomResourceAndStatus( + public static UpdateControl updateCustomResourceAndStatus( T customResource) { return new UpdateControl<>(customResource, true, true); } - public static > UpdateControl noUpdate() { + public static UpdateControl noUpdate() { return new UpdateControl<>(null, false, false); } - public T getCustomResource() { - return customResource; + public T getResource() { + return resource; } public boolean isUpdateStatusSubResource() { return updateStatusSubResource; } - public boolean isUpdateCustomResource() { - return updateCustomResource; + public boolean isUpdateResource() { + return updateResource; } public boolean isUpdateCustomResourceAndStatusSubResource() { - return updateCustomResource && updateStatusSubResource; + return updateResource && updateStatusSubResource; } } 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 cb0cef3b90..45d26e5a83 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 @@ -2,9 +2,9 @@ import java.util.Objects; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; -import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; @@ -22,7 +22,7 @@ import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; -public class Controller> implements Reconciler, +public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { private final Reconciler reconciler; private final ControllerConfiguration configuration; @@ -138,7 +138,7 @@ public KubernetesClient getClient() { } public MixedOperation, Resource> getCRClient() { - return kubernetesClient.resources(configuration.getCustomResourceClass()); + return kubernetesClient.resources(configuration.getResourceClass()); } /** @@ -150,9 +150,9 @@ public MixedOperation, Resource> getCRClient() { * @throws OperatorException if a problem occurred during the registration process */ public void start() throws OperatorException { - final Class resClass = configuration.getCustomResourceClass(); + final Class resClass = configuration.getResourceClass(); final String controllerName = configuration.getName(); - final var crdName = configuration.getCRDName(); + final var crdName = configuration.getResourceTypeName(); final var specVersion = "v1"; try { // check that the custom resource is known by the cluster if configured that way @@ -171,7 +171,7 @@ public void start() throws OperatorException { eventSourceManager = new EventSourceManager<>(this); eventProcessor = - new EventProcessor<>(this, eventSourceManager.getCustomResourceEventSource()); + new EventProcessor<>(this, eventSourceManager.getControllerResourceEventSource()); eventProcessor.setEventSourceManager(eventSourceManager); eventSourceManager.setEventProcessor(eventProcessor); if (reconciler instanceof EventSourceInitializer) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java index 9be023416b..b32983ac8e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java @@ -2,8 +2,8 @@ import java.util.HashMap; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; /** * Manages the state of received events. Basically there can be only three distinct states relevant @@ -22,33 +22,33 @@ public enum EventingState { DELETE_EVENT_PRESENT, } - private final HashMap eventingState = new HashMap<>(); + private final HashMap eventingState = new HashMap<>(); - private EventingState getEventingState(CustomResourceID customResourceID) { - EventingState actualState = eventingState.get(customResourceID); + private EventingState getEventingState(ResourceID resourceID) { + EventingState actualState = eventingState.get(resourceID); return actualState == null ? EventingState.NO_EVENT_PRESENT : actualState; } - private void setEventingState(CustomResourceID customResourceID, EventingState state) { - eventingState.put(customResourceID, state); + private void setEventingState(ResourceID resourceID, EventingState state) { + eventingState.put(resourceID, state); } public void markEventReceived(Event event) { markEventReceived(event.getRelatedCustomResourceID()); } - public void markEventReceived(CustomResourceID customResourceID) { - if (deleteEventPresent(customResourceID)) { + public void markEventReceived(ResourceID resourceID) { + if (deleteEventPresent(resourceID)) { throw new IllegalStateException("Cannot receive event after a delete event received"); } - setEventingState(customResourceID, EventingState.EVENT_PRESENT); + setEventingState(resourceID, EventingState.EVENT_PRESENT); } - public void unMarkEventReceived(CustomResourceID customResourceID) { - var actualState = getEventingState(customResourceID); + public void unMarkEventReceived(ResourceID resourceID) { + var actualState = getEventingState(resourceID); switch (actualState) { case EVENT_PRESENT: - setEventingState(customResourceID, + setEventingState(resourceID, EventingState.NO_EVENT_PRESENT); break; case DELETE_EVENT_PRESENT: @@ -60,25 +60,25 @@ public void markDeleteEventReceived(Event event) { markDeleteEventReceived(event.getRelatedCustomResourceID()); } - public void markDeleteEventReceived(CustomResourceID customResourceID) { - setEventingState(customResourceID, EventingState.DELETE_EVENT_PRESENT); + public void markDeleteEventReceived(ResourceID resourceID) { + setEventingState(resourceID, EventingState.DELETE_EVENT_PRESENT); } - public boolean deleteEventPresent(CustomResourceID customResourceID) { - return getEventingState(customResourceID) == EventingState.DELETE_EVENT_PRESENT; + public boolean deleteEventPresent(ResourceID resourceID) { + return getEventingState(resourceID) == EventingState.DELETE_EVENT_PRESENT; } - public boolean eventPresent(CustomResourceID customResourceID) { - var actualState = getEventingState(customResourceID); + public boolean eventPresent(ResourceID resourceID) { + var actualState = getEventingState(resourceID); return actualState == EventingState.EVENT_PRESENT; } - public boolean noEventPresent(CustomResourceID customResourceID) { - var actualState = getEventingState(customResourceID); + public boolean noEventPresent(ResourceID resourceID) { + var actualState = getEventingState(resourceID); return actualState == EventingState.NO_EVENT_PRESENT; } - public void cleanup(CustomResourceID customResourceID) { - eventingState.remove(customResourceID); + public void cleanup(ResourceID resourceID) { + eventingState.remove(resourceID); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java index 1fd8952ebf..360001602f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java @@ -12,19 +12,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEvent; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.processing.retry.Retry; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; @@ -36,15 +36,15 @@ * Event handler that makes sure that events are processed in a "single threaded" way per resource * UID, while buffering events which are received during an execution. */ -public class EventProcessor> +public class EventProcessor implements EventHandler, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventProcessor.class); - private final Set underProcessing = new HashSet<>(); + private final Set underProcessing = new HashSet<>(); private final ReconciliationDispatcher reconciliationDispatcher; private final Retry retry; - private final Map retryState = new HashMap<>(); + private final Map retryState = new HashMap<>(); private final ExecutorService executor; private final String controllerName; private final ReentrantLock lock = new ReentrantLock(); @@ -120,7 +120,7 @@ public void handleEvent(Event event) { } } - private void submitReconciliationExecution(CustomResourceID customResourceUid) { + private void submitReconciliationExecution(ResourceID customResourceUid) { try { boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); Optional latestCustomResource = @@ -156,15 +156,15 @@ private void submitReconciliationExecution(CustomResourceID customResourceUid) { } private void handleEventMarking(Event event) { - if (event instanceof CustomResourceEvent && - ((CustomResourceEvent) event).getAction() == ResourceAction.DELETED) { + if (event instanceof ResourceEvent && + ((ResourceEvent) event).getAction() == ResourceAction.DELETED) { eventMarker.markDeleteEventReceived(event); } else if (!eventMarker.deleteEventPresent(event.getRelatedCustomResourceID())) { eventMarker.markEventReceived(event); } } - private RetryInfo retryInfo(CustomResourceID customResourceUid) { + private RetryInfo retryInfo(ResourceID customResourceUid) { return retryState.get(customResourceUid); } @@ -175,36 +175,36 @@ void eventProcessingFinished( if (!running) { return; } - CustomResourceID customResourceID = executionScope.getCustomResourceID(); + ResourceID resourceID = executionScope.getCustomResourceID(); log.debug( "Event processing finished. Scope: {}, PostExecutionControl: {}", executionScope, postExecutionControl); - unsetUnderExecution(customResourceID); + unsetUnderExecution(resourceID); // If a delete event present at this phase, it was received during reconciliation. // So we either removed the finalizer during reconciliation or we don't use finalizers. // Either way we don't want to retry. if (isRetryConfigured() && postExecutionControl.exceptionDuringExecution() && - !eventMarker.deleteEventPresent(customResourceID)) { + !eventMarker.deleteEventPresent(resourceID)) { handleRetryOnException(executionScope, postExecutionControl.getRuntimeException().orElseThrow()); return; } cleanupOnSuccessfulExecution(executionScope); - metrics.finishedReconciliation(customResourceID); - if (eventMarker.deleteEventPresent(customResourceID)) { + metrics.finishedReconciliation(resourceID); + if (eventMarker.deleteEventPresent(resourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { - if (eventMarker.eventPresent(customResourceID)) { + if (eventMarker.eventPresent(resourceID)) { if (isCacheReadyForInstantReconciliation(executionScope, postExecutionControl)) { - submitReconciliationExecution(customResourceID); + submitReconciliationExecution(resourceID); } else { - postponeReconciliationAndHandleCacheSyncEvent(customResourceID); + postponeReconciliationAndHandleCacheSyncEvent(resourceID); } } else { reScheduleExecutionIfInstructed(postExecutionControl, - executionScope.getCustomResource()); + executionScope.getResource()); } } } finally { @@ -212,8 +212,8 @@ void eventProcessingFinished( } } - private void postponeReconciliationAndHandleCacheSyncEvent(CustomResourceID customResourceID) { - eventSourceManager.getCustomResourceEventSource().whitelistNextEvent(customResourceID); + private void postponeReconciliationAndHandleCacheSyncEvent(ResourceID resourceID) { + eventSourceManager.getControllerResourceEventSource().whitelistNextEvent(resourceID); } private boolean isCacheReadyForInstantReconciliation(ExecutionScope executionScope, @@ -221,7 +221,7 @@ private boolean isCacheReadyForInstantReconciliation(ExecutionScope execution if (!postExecutionControl.customResourceUpdatedDuringExecution()) { return true; } - String originalResourceVersion = getVersion(executionScope.getCustomResource()); + String originalResourceVersion = getVersion(executionScope.getResource()); String customResourceVersionAfterExecution = getVersion(postExecutionControl .getUpdatedCustomResource() .orElseThrow(() -> new IllegalStateException( @@ -277,7 +277,7 @@ private void handleRetryOnException(ExecutionScope executionScope, metrics.failedReconciliation(customResourceID, exception); eventSourceManager .getRetryAndRescheduleTimerEventSource() - .scheduleOnce(executionScope.getCustomResource(), delay); + .scheduleOnce(executionScope.getResource(), delay); }, () -> log.error("Exhausted retries for {}", executionScope)); } @@ -285,7 +285,7 @@ private void handleRetryOnException(ExecutionScope executionScope, private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) { log.debug( "Cleanup for successful execution for resource: {}", - getName(executionScope.getCustomResource())); + getName(executionScope.getResource())); if (isRetryConfigured()) { retryState.remove(executionScope.getCustomResourceID()); } @@ -303,21 +303,21 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) return retryExecution; } - private void cleanupForDeletedEvent(CustomResourceID customResourceUid) { + private void cleanupForDeletedEvent(ResourceID customResourceUid) { eventSourceManager.cleanupForCustomResource(customResourceUid); eventMarker.cleanup(customResourceUid); metrics.cleanupDoneFor(customResourceUid); } - private boolean isControllerUnderExecution(CustomResourceID customResourceUid) { + private boolean isControllerUnderExecution(ResourceID customResourceUid) { return underProcessing.contains(customResourceUid); } - private void setUnderExecutionProcessing(CustomResourceID customResourceUid) { + private void setUnderExecutionProcessing(ResourceID customResourceUid) { underProcessing.add(customResourceUid); } - private void unsetUnderExecution(CustomResourceID customResourceUid) { + private void unsetUnderExecution(ResourceID customResourceUid) { underProcessing.remove(customResourceUid); } @@ -358,7 +358,7 @@ public void run() { final var thread = Thread.currentThread(); final var name = thread.getName(); try { - MDCUtils.addCustomResourceInfo(executionScope.getCustomResource()); + MDCUtils.addCustomResourceInfo(executionScope.getResource()); thread.setName("EventHandler-" + controllerName); PostExecutionControl postExecutionControl = reconciliationDispatcher.handleExecution(executionScope); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java index a24f3461c7..cb40ab35eb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java @@ -1,35 +1,35 @@ package io.javaoperatorsdk.operator.processing; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.ResourceID; -public class ExecutionScope> { +public class ExecutionScope { // the latest custom resource from cache - private final R customResource; + private final R resource; private final RetryInfo retryInfo; - public ExecutionScope(R customResource, RetryInfo retryInfo) { - this.customResource = customResource; + public ExecutionScope(R resource, RetryInfo retryInfo) { + this.resource = resource; this.retryInfo = retryInfo; } - public R getCustomResource() { - return customResource; + public R getResource() { + return resource; } - public CustomResourceID getCustomResourceID() { - return CustomResourceID.fromResource(customResource); + public ResourceID getCustomResourceID() { + return ResourceID.fromResource(resource); } @Override public String toString() { return "ExecutionScope{" - + ", customResource uid: " - + customResource.getMetadata().getUid() + + " resource id: " + + ResourceID.fromResource(resource) + ", version: " - + customResource.getMetadata().getResourceVersion() + + resource.getMetadata().getResourceVersion() + '}'; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/KubernetesResourceUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/KubernetesResourceUtils.java index cf26dc08ed..f585974dff 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/KubernetesResourceUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/KubernetesResourceUtils.java @@ -4,15 +4,15 @@ public class KubernetesResourceUtils { - public static String getName(HasMetadata customResource) { - return customResource.getMetadata().getName(); + public static String getName(HasMetadata resource) { + return resource.getMetadata().getName(); } - public static String getUID(HasMetadata customResource) { - return customResource.getMetadata().getUid(); + public static String getUID(HasMetadata resource) { + return resource.getMetadata().getUid(); } - public static String getVersion(HasMetadata customResource) { - return customResource.getMetadata().getResourceVersion(); + public static String getVersion(HasMetadata resource) { + return resource.getMetadata().getResourceVersion(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java index e68312c451..6f475b56d8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java @@ -2,8 +2,8 @@ import org.slf4j.MDC; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; public class MDCUtils { @@ -15,9 +15,9 @@ public class MDCUtils { private static final String GENERATION = "resource.generation"; private static final String UID = "resource.uid"; - public static void addCustomResourceIDInfo(CustomResourceID customResourceID) { - MDC.put(NAME, customResourceID.getName()); - MDC.put(NAMESPACE, customResourceID.getNamespace().orElse("no namespace")); + public static void addCustomResourceIDInfo(ResourceID resourceID) { + MDC.put(NAME, resourceID.getName()); + MDC.put(NAMESPACE, resourceID.getNamespace().orElse("no namespace")); } public static void removeCustomResourceIDInfo() { @@ -25,14 +25,16 @@ public static void removeCustomResourceIDInfo() { MDC.remove(NAMESPACE); } - public static void addCustomResourceInfo(CustomResource customResource) { - MDC.put(API_VERSION, customResource.getApiVersion()); - MDC.put(KIND, customResource.getKind()); - MDC.put(NAME, customResource.getMetadata().getName()); - MDC.put(NAMESPACE, customResource.getMetadata().getNamespace()); - MDC.put(RESOURCE_VERSION, customResource.getMetadata().getResourceVersion()); - MDC.put(GENERATION, customResource.getMetadata().getGeneration().toString()); - MDC.put(UID, customResource.getMetadata().getUid()); + public static void addCustomResourceInfo(HasMetadata resource) { + MDC.put(API_VERSION, resource.getApiVersion()); + MDC.put(KIND, resource.getKind()); + MDC.put(NAME, resource.getMetadata().getName()); + MDC.put(NAMESPACE, resource.getMetadata().getNamespace()); + MDC.put(RESOURCE_VERSION, resource.getMetadata().getResourceVersion()); + if (resource.getMetadata().getGeneration() != null) { + MDC.put(GENERATION, resource.getMetadata().getGeneration().toString()); + } + MDC.put(UID, resource.getMetadata().getUid()); } public static void removeCustomResourceInfo() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java index 60d0f4bb34..e8df3225ef 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java @@ -2,9 +2,9 @@ import java.util.Optional; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; -public final class PostExecutionControl> { +public final class PostExecutionControl { private final boolean onlyFinalizerHandled; private final R updatedCustomResource; @@ -21,20 +21,20 @@ private PostExecutionControl( this.runtimeException = runtimeException; } - public static > PostExecutionControl onlyFinalizerAdded() { + public static PostExecutionControl onlyFinalizerAdded() { return new PostExecutionControl<>(true, null, null); } - public static > PostExecutionControl defaultDispatch() { + public static PostExecutionControl defaultDispatch() { return new PostExecutionControl<>(false, null, null); } - public static > PostExecutionControl customResourceUpdated( + public static PostExecutionControl customResourceUpdated( R updatedCustomResource) { return new PostExecutionControl<>(false, updatedCustomResource, null); } - public static > PostExecutionControl exceptionDuringExecution( + public static PostExecutionControl exceptionDuringExecution( RuntimeException exception) { return new PostExecutionControl<>(false, null, exception); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java index b38b880442..c198252678 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; @@ -25,7 +26,7 @@ /** * Handles calls and results of a Reconciler and finalizer related logic */ -public class ReconciliationDispatcher> { +public class ReconciliationDispatcher { private static final Logger log = LoggerFactory.getLogger(ReconciliationDispatcher.class); @@ -59,7 +60,7 @@ public PostExecutionControl handleExecution(ExecutionScope executionScope) } private PostExecutionControl handleDispatch(ExecutionScope executionScope) { - R resource = executionScope.getCustomResource(); + R resource = executionScope.getResource(); log.debug("Handling dispatch for resource {}", getName(resource)); final var markedForDeletion = resource.isMarkedForDeletion(); @@ -86,7 +87,7 @@ private ControllerConfiguration configuration() { /** * Determines whether the given resource should be dispatched to the controller's - * {@link Reconciler#cleanup(CustomResource, Context)} method + * {@link Reconciler#cleanup(HasMetadata, Context)} method * * @param resource the resource to be potentially deleted * @return {@code true} if the resource should be handed to the controller's {@code @@ -147,16 +148,16 @@ private PostExecutionControl createOrUpdateExecution(ExecutionScope execut UpdateControl updateControl = controller.reconcile(resource, context); R updatedCustomResource = null; if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { - updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); + updatedCustomResource = updateCustomResource(updateControl.getResource()); updateControl - .getCustomResource() + .getResource() .getMetadata() .setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion()); - updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); + updatedCustomResource = updateStatusGenerationAware(updateControl.getResource()); } else if (updateControl.isUpdateStatusSubResource()) { - updatedCustomResource = updateStatusGenerationAware(updateControl.getCustomResource()); - } else if (updateControl.isUpdateCustomResource()) { - updatedCustomResource = updateCustomResource(updateControl.getCustomResource()); + updatedCustomResource = updateStatusGenerationAware(updateControl.getResource()); + } else if (updateControl.isUpdateResource()) { + updatedCustomResource = updateCustomResource(updateControl.getResource()); } return createPostExecutionControl(updatedCustomResource, updateControl); } @@ -188,13 +189,17 @@ private R updateStatusGenerationAware(R customResource) { return customResourceFacade.updateStatus(customResource); } - private void updateStatusObservedGenerationIfRequired(R customResource) { - if (controller.getConfiguration().isGenerationAware()) { + private void updateStatusObservedGenerationIfRequired(R resource) { + // todo: change this to check for HasStatus (or similar) when + // https://github.com/fabric8io/kubernetes-client/issues/3586 is fixed + if (controller.getConfiguration().isGenerationAware() + && resource instanceof CustomResource) { + var customResource = (CustomResource) resource; var status = customResource.getStatus(); // Note that if status is null we won't update the observed generation. if (status instanceof ObservedGenerationAware) { ((ObservedGenerationAware) status) - .setObservedGeneration(customResource.getMetadata().getGeneration()); + .setObservedGeneration(resource.getMetadata().getGeneration()); } } } @@ -276,7 +281,7 @@ private R replace(R resource) { } // created to support unit testing - static class CustomResourceFacade> { + static class CustomResourceFacade { private final MixedOperation, Resource> resourceOperation; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java index 127926c724..8fa497f61c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java @@ -2,11 +2,11 @@ import java.util.Optional; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; -public interface ResourceCache> { +public interface ResourceCache { - Optional getCustomResource(CustomResourceID resourceID); + Optional getCustomResource(ResourceID resourceID); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java index ff871b1534..deacb85956 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java @@ -2,13 +2,13 @@ public class Event { - private final CustomResourceID relatedCustomResource; + private final ResourceID relatedCustomResource; - public Event(CustomResourceID targetCustomResource) { + public Event(ResourceID targetCustomResource) { this.relatedCustomResource = targetCustomResource; } - public CustomResourceID getRelatedCustomResourceID() { + public ResourceID getRelatedCustomResourceID() { return relatedCustomResource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java index 7646dcc353..a2be4547a2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java @@ -11,5 +11,5 @@ public interface EventSource extends LifecycleAware { * * @param customResourceUid - id of custom resource */ - default void cleanupForCustomResource(CustomResourceID customResourceUid) {} + default void cleanupForResource(ResourceID customResourceUid) {} } 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 b1d941d4e4..760bde2f43 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 @@ -9,16 +9,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.EventProcessor; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.internal.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; -public class EventSourceManager> +public class EventSourceManager implements EventSourceRegistry, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventSourceManager.class); @@ -27,7 +27,7 @@ public class EventSourceManager> private final Set eventSources = Collections.synchronizedSet(new HashSet<>()); private EventProcessor eventProcessor; private TimerEventSource retryAndRescheduleTimerEventSource; - private CustomResourceEventSource customResourceEventSource; + private ControllerResourceEventSource controllerResourceEventSource; EventSourceManager() { init(); @@ -35,8 +35,8 @@ public class EventSourceManager> public EventSourceManager(Controller controller) { init(); - customResourceEventSource = new CustomResourceEventSource<>(controller); - registerEventSource(customResourceEventSource); + controllerResourceEventSource = new ControllerResourceEventSource<>(controller); + registerEventSource(controllerResourceEventSource); } private void init() { @@ -46,8 +46,8 @@ private void init() { public EventSourceManager setEventProcessor(EventProcessor eventProcessor) { this.eventProcessor = eventProcessor; - if (customResourceEventSource != null) { - customResourceEventSource.setEventHandler(eventProcessor); + if (controllerResourceEventSource != null) { + controllerResourceEventSource.setEventHandler(eventProcessor); } if (retryAndRescheduleTimerEventSource != null) { retryAndRescheduleTimerEventSource.setEventHandler(eventProcessor); @@ -110,11 +110,11 @@ public final void registerEventSource(EventSource eventSource) } } - public void cleanupForCustomResource(CustomResourceID customResourceUid) { + public void cleanupForCustomResource(ResourceID customResourceUid) { lock.lock(); try { for (EventSource eventSource : this.eventSources) { - eventSource.cleanupForCustomResource(customResourceUid); + eventSource.cleanupForResource(customResourceUid); } } finally { lock.unlock(); @@ -131,8 +131,8 @@ public Set getRegisteredEventSources() { } @Override - public CustomResourceEventSource getCustomResourceEventSource() { - return customResourceEventSource; + public ControllerResourceEventSource getControllerResourceEventSource() { + return controllerResourceEventSource; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java index 95de04976e..518b470fa8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java @@ -2,11 +2,11 @@ import java.util.Set; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.internal.ControllerResourceEventSource; -public interface EventSourceRegistry> { +public interface EventSourceRegistry { /** * Add the {@link EventSource} identified by the given name to the event manager. @@ -21,6 +21,6 @@ void registerEventSource(EventSource eventSource) Set getRegisteredEventSources(); - CustomResourceEventSource getCustomResourceEventSource(); + ControllerResourceEventSource getControllerResourceEventSource(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java similarity index 76% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java index b668a772dd..515be245b8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/CustomResourceID.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java @@ -5,22 +5,22 @@ import io.fabric8.kubernetes.api.model.HasMetadata; -public class CustomResourceID { +public class ResourceID { - public static CustomResourceID fromResource(HasMetadata resource) { - return new CustomResourceID(resource.getMetadata().getName(), + public static ResourceID fromResource(HasMetadata resource) { + return new ResourceID(resource.getMetadata().getName(), resource.getMetadata().getNamespace()); } private final String name; private final String namespace; - public CustomResourceID(String name, String namespace) { + public ResourceID(String name, String namespace) { this.name = name; this.namespace = namespace; } - public CustomResourceID(String name) { + public ResourceID(String name) { this(name, null); } @@ -38,7 +38,7 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - CustomResourceID that = (CustomResourceID) o; + ResourceID that = (ResourceID) o; return Objects.equals(name, that.name) && Objects.equals(namespace, that.namespace); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSource.java similarity index 85% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSource.java index a912072e5c..775fd06948 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSource.java @@ -6,8 +6,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; @@ -20,7 +20,7 @@ import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.ResourceCache; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; @@ -29,31 +29,31 @@ /** * This is a special case since is not bound to a single custom resource */ -public class CustomResourceEventSource> extends AbstractEventSource +public class ControllerResourceEventSource extends AbstractEventSource implements ResourceEventHandler, ResourceCache { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; - private static final Logger log = LoggerFactory.getLogger(CustomResourceEventSource.class); + private static final Logger log = LoggerFactory.getLogger(ControllerResourceEventSource.class); private final Controller controller; private final Map> sharedIndexInformers = new ConcurrentHashMap<>(); - private final CustomResourceEventFilter filter; + private final ResourceEventFilter filter; private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; private final Cloner cloner; - public CustomResourceEventSource(Controller controller) { + public ControllerResourceEventSource(Controller controller) { this.controller = controller; this.cloner = controller.getConfiguration().getConfigurationService().getResourceCloner(); - var filters = new CustomResourceEventFilter[] { - CustomResourceEventFilters.finalizerNeededAndApplied(), - CustomResourceEventFilters.markedForDeletion(), - CustomResourceEventFilters.and( + var filters = new ResourceEventFilter[] { + ResourceEventFilters.finalizerNeededAndApplied(), + ResourceEventFilters.markedForDeletion(), + ResourceEventFilters.and( controller.getConfiguration().getEventFilter(), - CustomResourceEventFilters.generationAware()), + ResourceEventFilters.generationAware()), null }; @@ -63,7 +63,7 @@ public CustomResourceEventSource(Controller controller) { } else { onceWhitelistEventFilterEventFilter = null; } - filter = CustomResourceEventFilters.or(filters); + filter = ResourceEventFilters.or(filters); } @Override @@ -94,7 +94,7 @@ public void start() { KubernetesClientException ke = (KubernetesClientException) e; if (404 == ke.getCode()) { // only throw MissingCRDException if the 404 error occurs on the target CRD - final var targetCRDName = controller.getConfiguration().getCRDName(); + final var targetCRDName = controller.getConfiguration().getResourceTypeName(); if (targetCRDName.equals(ke.getFullResourceName())) { throw new MissingCRDException(targetCRDName, null, e.getMessage(), e); } @@ -132,7 +132,7 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource MDCUtils.addCustomResourceInfo(customResource); if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { eventHandler.handleEvent( - new CustomResourceEvent(action, CustomResourceID.fromResource(customResource))); + new ResourceEvent(action, ResourceID.fromResource(customResource))); } else { log.debug( "Skipping event handling resource {} with version: {}", @@ -160,7 +160,7 @@ public void onDelete(T resource, boolean b) { } @Override - public Optional getCustomResource(CustomResourceID resourceID) { + public Optional getCustomResource(ResourceID resourceID) { var sharedIndexInformer = sharedIndexInformers.get(ANY_NAMESPACE_MAP_KEY); if (sharedIndexInformer == null) { sharedIndexInformer = @@ -194,11 +194,11 @@ public SharedIndexInformer getInformer(String namespace) { * This will ensure that the next event received after this method is called will not be filtered * out. * - * @param customResourceID - to which the event is related + * @param resourceID - to which the event is related */ - public void whitelistNextEvent(CustomResourceID customResourceID) { + public void whitelistNextEvent(ResourceID resourceID) { if (onceWhitelistEventFilterEventFilter != null) { - onceWhitelistEventFilterEventFilter.whitelistNextEvent(customResourceID); + onceWhitelistEventFilterEventFilter.whitelistNextEvent(resourceID); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java index 9b61b18ade..8758566619 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java @@ -14,41 +14,41 @@ import io.fabric8.kubernetes.client.informers.cache.Cache; import io.fabric8.kubernetes.client.informers.cache.Store; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; public class InformerEventSource extends AbstractEventSource { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); private final SharedInformer sharedInformer; - private final Function> resourceToCustomResourceIDSet; + private final Function> secondaryToPrimaryResourcesIdSet; private final Function associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToCustomResourceIDSet) { - this(sharedInformer, resourceToCustomResourceIDSet, null, true); + Function> resourceToTargetResourceIDSet) { + this(sharedInformer, resourceToTargetResourceIDSet, null, true); } public InformerEventSource(KubernetesClient client, Class type, - Function> resourceToCustomResourceIDSet) { - this(client, type, resourceToCustomResourceIDSet, false); + Function> resourceToTargetResourceIDSet) { + this(client, type, resourceToTargetResourceIDSet, false); } InformerEventSource(KubernetesClient client, Class type, - Function> resourceToCustomResourceIDSet, + Function> resourceToTargetResourceIDSet, boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToCustomResourceIDSet, null, + this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, null, skipUpdateEventPropagationIfNoChange); } public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToCustomResourceIDSet, + Function> resourceToTargetResourceIDSet, Function associatedWith, boolean skipUpdateEventPropagationIfNoChange) { this.sharedInformer = sharedInformer; - this.resourceToCustomResourceIDSet = resourceToCustomResourceIDSet; + this.secondaryToPrimaryResourcesIdSet = resourceToTargetResourceIDSet; this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; if (sharedInformer.isRunning()) { log.warn( @@ -86,12 +86,12 @@ public void onDelete(T t, boolean b) { } private void propagateEvent(T object) { - var customResourceIDSet = resourceToCustomResourceIDSet.apply(object); - if (customResourceIDSet.isEmpty()) { + var primaryResourceIdSet = secondaryToPrimaryResourcesIdSet.apply(object); + if (primaryResourceIdSet.isEmpty()) { return; } - customResourceIDSet.forEach(customResourceId -> { - Event event = new Event(customResourceId); + primaryResourceIdSet.forEach(resourceId -> { + Event event = new Event(resourceId); /* * In fabric8 client for certain cases informers can be created on in a way that they are * automatically started, what would cause a NullPointerException here, since an event might diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java index 7351b80d4f..07d3b05cfe 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java @@ -5,31 +5,31 @@ import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.ResourceID; public class Mappers { - public static Function> fromAnnotation( + public static Function> fromAnnotation( String nameKey) { return fromMetadata(nameKey, null, false); } - public static Function> fromAnnotation( + public static Function> fromAnnotation( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, false); } - public static Function> fromLabel( + public static Function> fromLabel( String nameKey) { return fromMetadata(nameKey, null, true); } - public static Function> fromLabel( + public static Function> fromLabel( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, true); } - private static Function> fromMetadata( + private static Function> fromMetadata( String nameKey, String namespaceKey, boolean isLabel) { return resource -> { final var metadata = resource.getMetadata(); @@ -39,7 +39,7 @@ private static Function> fromMe final var map = isLabel ? metadata.getLabels() : metadata.getAnnotations(); var namespace = namespaceKey == null ? resource.getMetadata().getNamespace() : map.get(namespaceKey); - return map != null ? Set.of(new CustomResourceID(map.get(nameKey), namespace)) + return map != null ? Set.of(new ResourceID(map.get(nameKey), namespace)) : Collections.emptySet(); } }; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java index 4c144e65e6..34c5514c39 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java @@ -6,31 +6,30 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.ResourceID; -public class OnceWhitelistEventFilterEventFilter> - implements CustomResourceEventFilter { +public class OnceWhitelistEventFilterEventFilter + implements ResourceEventFilter { private static final Logger log = LoggerFactory.getLogger(OnceWhitelistEventFilterEventFilter.class); - private final ConcurrentMap whiteList = - new ConcurrentHashMap<>(); + private final ConcurrentMap whiteList = new ConcurrentHashMap<>(); @Override public boolean acceptChange(ControllerConfiguration configuration, T oldResource, T newResource) { - CustomResourceID customResourceID = CustomResourceID.fromResource(newResource); - boolean res = whiteList.remove(customResourceID, customResourceID); + ResourceID resourceID = ResourceID.fromResource(newResource); + boolean res = whiteList.remove(resourceID, resourceID); if (res) { - log.debug("Accepting whitelisted event for CR id: {}", customResourceID); + log.debug("Accepting whitelisted event for CR id: {}", resourceID); } return res; } - public void whitelistNextEvent(CustomResourceID customResourceID) { - whiteList.putIfAbsent(customResourceID, customResourceID); + public void whitelistNextEvent(ResourceID resourceID) { + whiteList.putIfAbsent(resourceID, resourceID); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEvent.java similarity index 61% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEvent.java index 9752e7cf5e..c15792f867 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEvent.java @@ -1,15 +1,15 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; -public class CustomResourceEvent extends Event { +public class ResourceEvent extends Event { private final ResourceAction action; - public CustomResourceEvent(ResourceAction action, - CustomResourceID customResourceID) { - super(customResourceID); + public ResourceEvent(ResourceAction action, + ResourceID resourceID) { + super(resourceID); this.action = action; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilter.java similarity index 89% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilter.java index cf9802674e..b4c101be00 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilter.java @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.internal; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; /** @@ -11,7 +11,7 @@ * @param the type of custom resources handled by this filter */ @FunctionalInterface -public interface CustomResourceEventFilter> { +public interface ResourceEventFilter { /** * Determines whether the change between the old version of the resource and the new one needs to @@ -32,7 +32,7 @@ public interface CustomResourceEventFilter> { * @param other the possibly {@code null} other filter to combine this one with * @return a composite filter implementing the AND logic between this and the provided filter */ - default CustomResourceEventFilter and(CustomResourceEventFilter other) { + default ResourceEventFilter and(ResourceEventFilter other) { return other == null ? this : (ControllerConfiguration configuration, T oldResource, T newResource) -> { boolean result = acceptChange(configuration, oldResource, newResource); @@ -48,7 +48,7 @@ default CustomResourceEventFilter and(CustomResourceEventFilter other) { * @param other the possibly {@code null} other filter to combine this one with * @return a composite filter implementing the OR logic between this and the provided filter */ - default CustomResourceEventFilter or(CustomResourceEventFilter other) { + default ResourceEventFilter or(ResourceEventFilter other) { return other == null ? this : (ControllerConfiguration configuration, T oldResource, T newResource) -> { boolean result = acceptChange(configuration, oldResource, newResource); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java similarity index 67% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java index 63584a2c86..a46a9175e6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java @@ -1,14 +1,15 @@ package io.javaoperatorsdk.operator.processing.event.internal; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; /** - * Convenience implementations of, and utility methods for, {@link CustomResourceEventFilter}. + * Convenience implementations of, and utility methods for, {@link ResourceEventFilter}. */ -public final class CustomResourceEventFilters { +public final class ResourceEventFilters { - private static final CustomResourceEventFilter> USE_FINALIZER = + private static final ResourceEventFilter USE_FINALIZER = (configuration, oldResource, newResource) -> { if (configuration.useFinalizer()) { final var finalizer = configuration.getFinalizer(); @@ -21,30 +22,35 @@ public final class CustomResourceEventFilters { } }; - private static final CustomResourceEventFilter> GENERATION_AWARE = + private static final ResourceEventFilter GENERATION_AWARE = (configuration, oldResource, newResource) -> { - final var status = newResource.getStatus(); final var generationAware = configuration.isGenerationAware(); - if (generationAware && status instanceof ObservedGenerationAware) { - var actualGeneration = newResource.getMetadata().getGeneration(); - var observedGeneration = ((ObservedGenerationAware) status) - .getObservedGeneration(); - return observedGeneration.map(aLong -> actualGeneration > aLong).orElse(true); + // todo: change this to check for HasStatus (or similar) when + // https://github.com/fabric8io/kubernetes-client/issues/3586 is fixed + if (newResource instanceof CustomResource) { + var newCustomResource = (CustomResource) newResource; + final var status = newCustomResource.getStatus(); + if (generationAware && status instanceof ObservedGenerationAware) { + var actualGeneration = newResource.getMetadata().getGeneration(); + var observedGeneration = ((ObservedGenerationAware) status) + .getObservedGeneration(); + return observedGeneration.map(aLong -> actualGeneration > aLong).orElse(true); + } } return oldResource == null || !generationAware || oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); }; - private static final CustomResourceEventFilter> PASSTHROUGH = + private static final ResourceEventFilter PASSTHROUGH = (configuration, oldResource, newResource) -> true; - private static final CustomResourceEventFilter> NONE = + private static final ResourceEventFilter NONE = (configuration, oldResource, newResource) -> false; - private static final CustomResourceEventFilter> MARKED_FOR_DELETION = + private static final ResourceEventFilter MARKED_FOR_DELETION = (configuration, oldResource, newResource) -> newResource.isMarkedForDeletion(); - private CustomResourceEventFilters() {} + private ResourceEventFilters() {} /** * Retrieves a filter that accepts all events. @@ -53,8 +59,8 @@ private CustomResourceEventFilters() {} * @return a filter that accepts all events */ @SuppressWarnings("unchecked") - public static > CustomResourceEventFilter passthrough() { - return (CustomResourceEventFilter) PASSTHROUGH; + public static ResourceEventFilter passthrough() { + return (ResourceEventFilter) PASSTHROUGH; } /** @@ -64,8 +70,8 @@ private CustomResourceEventFilters() {} * @return a filter that reject all events */ @SuppressWarnings("unchecked") - public static > CustomResourceEventFilter none() { - return (CustomResourceEventFilter) NONE; + public static ResourceEventFilter none() { + return (ResourceEventFilter) NONE; } /** @@ -76,8 +82,8 @@ private CustomResourceEventFilters() {} * @return a filter accepting changes based on generation information */ @SuppressWarnings("unchecked") - public static > CustomResourceEventFilter generationAware() { - return (CustomResourceEventFilter) GENERATION_AWARE; + public static ResourceEventFilter generationAware() { + return (ResourceEventFilter) GENERATION_AWARE; } /** @@ -89,8 +95,8 @@ private CustomResourceEventFilters() {} * applied */ @SuppressWarnings("unchecked") - public static > CustomResourceEventFilter finalizerNeededAndApplied() { - return (CustomResourceEventFilter) USE_FINALIZER; + public static ResourceEventFilter finalizerNeededAndApplied() { + return (ResourceEventFilter) USE_FINALIZER; } /** @@ -100,14 +106,14 @@ private CustomResourceEventFilters() {} * @return a filter accepting changes based on whether the Custom Resource is marked for deletion. */ @SuppressWarnings("unchecked") - public static > CustomResourceEventFilter markedForDeletion() { - return (CustomResourceEventFilter) MARKED_FOR_DELETION; + public static ResourceEventFilter markedForDeletion() { + return (ResourceEventFilter) MARKED_FOR_DELETION; } /** * Combines the provided, potentially {@code null} filters with an AND logic, i.e. the resulting * filter will only accept the change if all filters accept it, reject it otherwise. - * + *

* Note that the evaluation of filters is lazy: the result is returned as soon as possible without * evaluating all filters if possible. * @@ -116,14 +122,14 @@ private CustomResourceEventFilters() {} * @return a combined filter implementing the AND logic combination of the provided filters */ @SafeVarargs - public static > CustomResourceEventFilter and( - CustomResourceEventFilter... items) { + public static ResourceEventFilter and( + ResourceEventFilter... items) { if (items == null) { return none(); } return (configuration, oldResource, newResource) -> { - for (CustomResourceEventFilter item : items) { + for (ResourceEventFilter item : items) { if (item == null) { continue; } @@ -150,14 +156,14 @@ private CustomResourceEventFilters() {} * @return a combined filter implementing the OR logic combination of both provided filters */ @SafeVarargs - public static > CustomResourceEventFilter or( - CustomResourceEventFilter... items) { + public static ResourceEventFilter or( + ResourceEventFilter... items) { if (items == null) { return none(); } return (configuration, oldResource, newResource) -> { - for (CustomResourceEventFilter item : items) { + for (ResourceEventFilter item : items) { if (item == null) { continue; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java index 7fda09df1f..030b46298e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java @@ -9,24 +9,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; -public class TimerEventSource> extends AbstractEventSource { +public class TimerEventSource extends AbstractEventSource { private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class); private final Timer timer = new Timer(); private final AtomicBoolean running = new AtomicBoolean(); - private final Map onceTasks = new ConcurrentHashMap<>(); + private final Map onceTasks = new ConcurrentHashMap<>(); - public void scheduleOnce(R customResource, long delay) { + public void scheduleOnce(R resource, long delay) { if (!running.get()) { throw new IllegalStateException("The TimerEventSource is not running"); } - CustomResourceID resourceUid = CustomResourceID.fromResource(customResource); + ResourceID resourceUid = ResourceID.fromResource(resource); if (onceTasks.containsKey(resourceUid)) { cancelOnceSchedule(resourceUid); } @@ -36,11 +36,11 @@ public void scheduleOnce(R customResource, long delay) { } @Override - public void cleanupForCustomResource(CustomResourceID customResourceUid) { - cancelOnceSchedule(customResourceUid); + public void cleanupForResource(ResourceID resourceUid) { + cancelOnceSchedule(resourceUid); } - public void cancelOnceSchedule(CustomResourceID customResourceUid) { + public void cancelOnceSchedule(ResourceID customResourceUid) { TimerTask timerTask = onceTasks.remove(customResourceUid); if (timerTask != null) { timerTask.cancel(); @@ -61,9 +61,9 @@ public void stop() { public class EventProducerTimeTask extends TimerTask { - protected final CustomResourceID customResourceUid; + protected final ResourceID customResourceUid; - public EventProducerTimeTask(CustomResourceID customResourceUid) { + public EventProducerTimeTask(ResourceID customResourceUid) { this.customResourceUid = customResourceUid; } 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 6db84b45e3..c24a6dbe3e 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 @@ -2,6 +2,7 @@ import org.junit.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.Operator.ControllerManager; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; @@ -39,7 +40,7 @@ public void addingMultipleControllersForCustomResourcesWithDifferentVersionsShou } - private , U extends CustomResource> void checkException( + private void checkException( TestControllerConfiguration registered, TestControllerConfiguration duplicated) { final var exception = assertThrows(OperatorException.class, () -> { @@ -51,10 +52,10 @@ public void addingMultipleControllersForCustomResourcesWithDifferentVersionsShou assertTrue( msg.contains("Cannot register controller '" + duplicated.getControllerName() + "'") && msg.contains(registered.getControllerName()) - && msg.contains(registered.getCRDName())); + && msg.contains(registered.getResourceTypeName())); } - private static class TestControllerConfiguration> + private static class TestControllerConfiguration extends DefaultControllerConfiguration { private final Reconciler controller; @@ -64,7 +65,7 @@ public TestControllerConfiguration(Reconciler controller, Class crClass) { this.controller = controller; } - static > String getControllerName( + static String getControllerName( Reconciler controller) { return controller.getClass().getSimpleName() + "Controller"; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index 5e15fd59c8..137ed2d11d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -6,14 +6,14 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinitionBuilder; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceSpec; public class TestUtils { public static TestCustomResource testCustomResource() { - return testCustomResource(new CustomResourceID(UUID.randomUUID().toString(), "test")); + return testCustomResource(new ResourceID(UUID.randomUUID().toString(), "test")); } public static CustomResourceDefinition testCRD(String scope) { @@ -27,7 +27,7 @@ public static CustomResourceDefinition testCRD(String scope) { .build(); } - public static TestCustomResource testCustomResource(CustomResourceID id) { + public static TestCustomResource testCustomResource(ResourceID id) { TestCustomResource resource = new TestCustomResource(); resource.setMetadata( new ObjectMetaBuilder() diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java index 7f8698d81b..3bc72d81f2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -21,6 +21,6 @@ public ConfigurationService getConfigurationService() { return null; } }; - assertEquals(TestCustomResource.class, conf.getCustomResourceClass()); + assertEquals(TestCustomResource.class, conf.getResourceClass()); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java index 98b87f1713..08090f3c1d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java @@ -3,64 +3,64 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import static org.assertj.core.api.Assertions.assertThat; class EventMarkerTest { private final EventMarker eventMarker = new EventMarker(); - private CustomResourceID sampleCustomResourceID = new CustomResourceID("test-name"); + private ResourceID sampleResourceID = new ResourceID("test-name"); @Test public void returnsNoEventPresentIfNotMarkedYet() { - assertThat(eventMarker.noEventPresent(sampleCustomResourceID)).isTrue(); + assertThat(eventMarker.noEventPresent(sampleResourceID)).isTrue(); } @Test public void marksEvent() { - eventMarker.markEventReceived(sampleCustomResourceID); + eventMarker.markEventReceived(sampleResourceID); - assertThat(eventMarker.eventPresent(sampleCustomResourceID)).isTrue(); - assertThat(eventMarker.deleteEventPresent(sampleCustomResourceID)).isFalse(); + assertThat(eventMarker.eventPresent(sampleResourceID)).isTrue(); + assertThat(eventMarker.deleteEventPresent(sampleResourceID)).isFalse(); } @Test public void marksDeleteEvent() { - eventMarker.markDeleteEventReceived(sampleCustomResourceID); + eventMarker.markDeleteEventReceived(sampleResourceID); - assertThat(eventMarker.deleteEventPresent(sampleCustomResourceID)) + assertThat(eventMarker.deleteEventPresent(sampleResourceID)) .isTrue(); - assertThat(eventMarker.eventPresent(sampleCustomResourceID)).isFalse(); + assertThat(eventMarker.eventPresent(sampleResourceID)).isFalse(); } @Test public void afterDeleteEventMarkEventIsNotRelevant() { - eventMarker.markEventReceived(sampleCustomResourceID); + eventMarker.markEventReceived(sampleResourceID); - eventMarker.markDeleteEventReceived(sampleCustomResourceID); + eventMarker.markDeleteEventReceived(sampleResourceID); - assertThat(eventMarker.deleteEventPresent(sampleCustomResourceID)) + assertThat(eventMarker.deleteEventPresent(sampleResourceID)) .isTrue(); - assertThat(eventMarker.eventPresent(sampleCustomResourceID)).isFalse(); + assertThat(eventMarker.eventPresent(sampleResourceID)).isFalse(); } @Test public void cleansUp() { - eventMarker.markEventReceived(sampleCustomResourceID); - eventMarker.markDeleteEventReceived(sampleCustomResourceID); + eventMarker.markEventReceived(sampleResourceID); + eventMarker.markDeleteEventReceived(sampleResourceID); - eventMarker.cleanup(sampleCustomResourceID); + eventMarker.cleanup(sampleResourceID); - assertThat(eventMarker.deleteEventPresent(sampleCustomResourceID)).isFalse(); - assertThat(eventMarker.eventPresent(sampleCustomResourceID)).isFalse(); + assertThat(eventMarker.deleteEventPresent(sampleResourceID)).isFalse(); + assertThat(eventMarker.eventPresent(sampleResourceID)).isFalse(); } @Test public void cannotMarkEventAfterDeleteEventReceived() { Assertions.assertThrows(IllegalStateException.class, () -> { - eventMarker.markDeleteEventReceived(sampleCustomResourceID); - eventMarker.markEventReceived(sampleCustomResourceID); + eventMarker.markDeleteEventReceived(sampleResourceID); + eventMarker.markEventReceived(sampleResourceID); }); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java index 2c10a37ddc..2e3c708c6c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java @@ -11,13 +11,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEvent; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.internal.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEvent; import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -80,7 +80,7 @@ public void skipProcessingIfLatestCustomResourceNotInCache() { @Test public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedException { - CustomResourceID resourceUid = eventAlreadyUnderProcessing(); + ResourceID resourceUid = eventAlreadyUnderProcessing(); eventProcessor.handleEvent(nonCREvent(resourceUid)); @@ -205,7 +205,7 @@ public void doNotFireEventsIfClosing() { @Test public void cleansUpWhenDeleteEventReceivedAndNoEventPresent() { Event deleteEvent = - new CustomResourceEvent(DELETED, prepareCREvent().getRelatedCustomResourceID()); + new ResourceEvent(DELETED, prepareCREvent().getRelatedCustomResourceID()); eventProcessor.handleEvent(deleteEvent); @@ -216,7 +216,7 @@ public void cleansUpWhenDeleteEventReceivedAndNoEventPresent() { @Test public void cleansUpAfterExecutionIfOnlyDeleteEventMarkLeft() { var cr = testCustomResource(); - var crEvent = prepareCREvent(CustomResourceID.fromResource(cr)); + var crEvent = prepareCREvent(ResourceID.fromResource(cr)); eventMarker.markDeleteEventReceived(crEvent.getRelatedCustomResourceID()); var executionScope = new ExecutionScope(cr, null); @@ -229,14 +229,14 @@ public void cleansUpAfterExecutionIfOnlyDeleteEventMarkLeft() { @Test public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { - var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); + var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); var updatedCr = testCustomResource(crID); updatedCr.getMetadata().setResourceVersion("2"); - var mockCREventSource = mock(CustomResourceEventSource.class); + var mockCREventSource = mock(ControllerResourceEventSource.class); eventMarker.markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getCustomResourceEventSource()) + when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(mockCREventSource); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), @@ -247,16 +247,16 @@ public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { @Test public void dontWhitelistsEventWhenOtherChangeDuringExecution() { - var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); + var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); var updatedCr = testCustomResource(crID); updatedCr.getMetadata().setResourceVersion("2"); var otherChangeCR = testCustomResource(crID); otherChangeCR.getMetadata().setResourceVersion("3"); - var mockCREventSource = mock(CustomResourceEventSource.class); + var mockCREventSource = mock(ControllerResourceEventSource.class); eventMarker.markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(otherChangeCR)); - when(eventSourceManagerMock.getCustomResourceEventSource()) + when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(mockCREventSource); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), @@ -267,12 +267,12 @@ public void dontWhitelistsEventWhenOtherChangeDuringExecution() { @Test public void dontWhitelistsEventIfUpdatedEventInCache() { - var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); + var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); - var mockCREventSource = mock(CustomResourceEventSource.class); + var mockCREventSource = mock(ControllerResourceEventSource.class); eventMarker.markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getCustomResourceEventSource()) + when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(mockCREventSource); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), @@ -283,7 +283,7 @@ public void dontWhitelistsEventIfUpdatedEventInCache() { @Test public void cancelScheduleOnceEventsOnSuccessfulExecution() { - var crID = new CustomResourceID("test-cr", TEST_NAMESPACE); + var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), @@ -292,7 +292,7 @@ public void cancelScheduleOnceEventsOnSuccessfulExecution() { verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); } - private CustomResourceID eventAlreadyUnderProcessing() { + private ResourceID eventAlreadyUnderProcessing() { when(reconciliationDispatcherMock.handleExecution(any())) .then( (Answer) invocationOnMock -> { @@ -304,22 +304,22 @@ private CustomResourceID eventAlreadyUnderProcessing() { return event.getRelatedCustomResourceID(); } - private CustomResourceEvent prepareCREvent() { - return prepareCREvent(new CustomResourceID(UUID.randomUUID().toString(), TEST_NAMESPACE)); + private ResourceEvent prepareCREvent() { + return prepareCREvent(new ResourceID(UUID.randomUUID().toString(), TEST_NAMESPACE)); } - private CustomResourceEvent prepareCREvent(CustomResourceID uid) { + private ResourceEvent prepareCREvent(ResourceID uid) { TestCustomResource customResource = testCustomResource(uid); when(resourceCacheMock.getCustomResource(eq(uid))).thenReturn(Optional.of(customResource)); - return new CustomResourceEvent(ResourceAction.UPDATED, - CustomResourceID.fromResource(customResource)); + return new ResourceEvent(ResourceAction.UPDATED, + ResourceID.fromResource(customResource)); } - private Event nonCREvent(CustomResourceID relatedCustomResourceUid) { + private Event nonCREvent(ResourceID relatedCustomResourceUid) { return new Event(relatedCustomResourceUid); } - private void overrideData(CustomResourceID id, CustomResource applyTo) { + private void overrideData(ResourceID id, HasMetadata applyTo) { applyTo.getMetadata().setName(id.getName()); applyTo.getMetadata().setNamespace(id.getNamespace().orElse(null)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java index 781b48a53b..052ac91a0e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java @@ -7,6 +7,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; @@ -46,7 +47,7 @@ void setup() { init(testCustomResource, reconciler, configuration, customResourceFacade); } - private > ReconciliationDispatcher init(R customResource, + private ReconciliationDispatcher init(R customResource, Reconciler reconciler, ControllerConfiguration configuration, CustomResourceFacade customResourceFacade) { when(configuration.getFinalizer()).thenReturn(DEFAULT_FINALIZER); @@ -56,7 +57,7 @@ void setup() { when(configuration.getConfigurationService()).thenReturn(configService); when(configService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); when(reconciler.reconcile(eq(customResource), any())) - .thenReturn(UpdateControl.updateCustomResource(customResource)); + .thenReturn(UpdateControl.updateResource(customResource)); when(reconciler.cleanup(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); when(customResourceFacade.replaceWithLock(any())).thenReturn(null); @@ -156,7 +157,7 @@ void doNotCallDeleteIfMarkedForDeletionWhenFinalizerHasAlreadyBeenRemoved() { } private void configureToNotUseFinalizer() { - ControllerConfiguration> configuration = + ControllerConfiguration configuration = mock(ControllerConfiguration.class); when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configService.getMetrics()).thenReturn(Metrics.NOOP); @@ -371,7 +372,7 @@ private void removeFinalizers(CustomResource customResource) { customResource.getMetadata().getFinalizers().clear(); } - public > ExecutionScope executionScopeWithCREvent(T resource) { + public ExecutionScope executionScopeWithCREvent(T resource) { return new ExecutionScope<>(resource, null); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index 76dc7e3411..2054efd10b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -73,9 +73,9 @@ public void deRegistersEventSources() { eventSourceManager.registerEventSource(eventSource); eventSourceManager - .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); + .cleanupForCustomResource(ResourceID.fromResource(customResource)); verify(eventSource, times(1)) - .cleanupForCustomResource(eq(CustomResourceID.fromResource(customResource))); + .cleanupForResource(eq(ResourceID.fromResource(customResource))); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSourceTest.java similarity index 76% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSourceTest.java index 61a550a652..5fb4b65dfb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSourceTest.java @@ -14,8 +14,8 @@ import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.Mockito.any; @@ -24,19 +24,19 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -class CustomResourceEventSourceTest { +class ControllerResourceEventSourceTest { public static final String FINALIZER = "finalizer"; private static final MixedOperation, Resource> client = mock(MixedOperation.class); EventHandler eventHandler = mock(EventHandler.class); - private CustomResourceEventSource customResourceEventSource = - new CustomResourceEventSource<>(new TestController(true)); + private ControllerResourceEventSource controllerResourceEventSource = + new ControllerResourceEventSource<>(new TestController(true)); @BeforeEach public void setup() { - customResourceEventSource.setEventHandler(eventHandler); + controllerResourceEventSource.setEventHandler(eventHandler); } @Test @@ -48,11 +48,11 @@ public void skipsEventHandlingIfGenerationNotIncreased() { TestCustomResource oldCustomResource = TestUtils.testCustomResource(); oldCustomResource.getMetadata().setFinalizers(List.of(FINALIZER)); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, oldCustomResource); verify(eventHandler, times(1)).handleEvent(any()); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, customResource); verify(eventHandler, times(1)).handleEvent(any()); } @@ -61,13 +61,13 @@ public void skipsEventHandlingIfGenerationNotIncreased() { public void dontSkipEventHandlingIfMarkedForDeletion() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(1)).handleEvent(any()); // mark for deletion customResource1.getMetadata().setDeletionTimestamp(LocalDateTime.now().toString()); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @@ -76,29 +76,29 @@ public void dontSkipEventHandlingIfMarkedForDeletion() { public void normalExecutionIfGenerationChanges() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(1)).handleEvent(any()); customResource1.getMetadata().setGeneration(2L); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @Test public void handlesAllEventIfNotGenerationAware() { - customResourceEventSource = - new CustomResourceEventSource<>(new TestController(false)); + controllerResourceEventSource = + new ControllerResourceEventSource<>(new TestController(false)); setup(); TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(1)).handleEvent(any()); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @@ -107,7 +107,7 @@ public void handlesAllEventIfNotGenerationAware() { public void eventWithNoGenerationProcessedIfNoFinalizer() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(1)).handleEvent(any()); @@ -117,9 +117,9 @@ public void eventWithNoGenerationProcessedIfNoFinalizer() { public void handlesNextEventIfWhitelisted() { TestCustomResource customResource = TestUtils.testCustomResource(); customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - customResourceEventSource.whitelistNextEvent(CustomResourceID.fromResource(customResource)); + controllerResourceEventSource.whitelistNextEvent(ResourceID.fromResource(customResource)); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, customResource); verify(eventHandler, times(1)).handleEvent(any()); @@ -130,7 +130,7 @@ public void notHandlesNextEventIfNotWhitelisted() { TestCustomResource customResource = TestUtils.testCustomResource(); customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - customResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, customResource); verify(eventHandler, times(0)).handleEvent(any()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java index fde6bb2a7f..5634f11cad 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; import static org.assertj.core.api.Assertions.assertThat; @@ -22,7 +22,7 @@ public void notAcceptCustomResourceNotWhitelisted() { public void allowCustomResourceWhitelisted() { var cr = TestUtils.testCustomResource(); - filter.whitelistNextEvent(CustomResourceID.fromResource(cr)); + filter.whitelistNextEvent(ResourceID.fromResource(cr)); assertThat(filter.acceptChange(null, cr, cr)).isTrue(); @@ -32,7 +32,7 @@ public void allowCustomResourceWhitelisted() { public void allowCustomResourceWhitelistedOnlyOnce() { var cr = TestUtils.testCustomResource(); - filter.whitelistNextEvent(CustomResourceID.fromResource(cr)); + filter.whitelistNextEvent(ResourceID.fromResource(cr)); assertThat(filter.acceptChange(null, cr, cr)).isTrue(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilterTest.java similarity index 91% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilterTest.java index 119d683880..1c130a0f6b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilterTest.java @@ -6,9 +6,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; @@ -26,7 +26,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -class CustomResourceEventFilterTest { +class ResourceEventFilterTest { public static final String FINALIZER = "finalizer"; private EventHandler eventHandler; @@ -46,7 +46,7 @@ public void eventFilteredByCustomPredicate() { newResource.getStatus().getConfigMapStatus())); var controller = new TestController(config); - var eventSource = new CustomResourceEventSource<>(controller); + var eventSource = new ControllerResourceEventSource<>(controller); eventSource.setEventHandler(eventHandler); TestCustomResource cr = TestUtils.testCustomResource(); @@ -74,7 +74,7 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { newResource.getStatus().getConfigMapStatus())); var controller = new TestController(config); - var eventSource = new CustomResourceEventSource<>(controller); + var eventSource = new ControllerResourceEventSource<>(controller); eventSource.setEventHandler(eventHandler); TestCustomResource cr = TestUtils.testCustomResource(); @@ -104,7 +104,7 @@ public void observedGenerationFiltering() { .thenReturn(ConfigurationService.DEFAULT_CLONER); var controller = new ObservedGenController(config); - var eventSource = new CustomResourceEventSource<>(controller); + var eventSource = new ControllerResourceEventSource<>(controller); eventSource.setEventHandler(eventHandler); ObservedGenCustomResource cr = new ObservedGenCustomResource(); @@ -135,7 +135,7 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { .thenReturn(ConfigurationService.DEFAULT_CLONER); var controller = new TestController(config); - var eventSource = new CustomResourceEventSource<>(controller); + var eventSource = new ControllerResourceEventSource<>(controller); eventSource.setEventHandler(eventHandler); TestCustomResource cr = TestUtils.testCustomResource(); @@ -154,23 +154,23 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { private static class TestControllerConfig extends ControllerConfig { public TestControllerConfig(String finalizer, boolean generationAware, - CustomResourceEventFilter eventFilter) { + ResourceEventFilter eventFilter) { super(finalizer, generationAware, eventFilter, TestCustomResource.class); } } private static class ObservedGenControllerConfig extends ControllerConfig { public ObservedGenControllerConfig(String finalizer, boolean generationAware, - CustomResourceEventFilter eventFilter) { + ResourceEventFilter eventFilter) { super(finalizer, generationAware, eventFilter, ObservedGenCustomResource.class); } } - private static class ControllerConfig> extends + private static class ControllerConfig extends DefaultControllerConfiguration { public ControllerConfig(String finalizer, boolean generationAware, - CustomResourceEventFilter eventFilter, Class customResourceClass) { + ResourceEventFilter eventFilter, Class customResourceClass) { super( null, null, diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java index c862843a05..e41c78ba55 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java @@ -12,9 +12,9 @@ import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; @@ -52,7 +52,7 @@ public void canCancelOnce() { TestCustomResource customResource = TestUtils.testCustomResource(); timerEventSource.scheduleOnce(customResource, PERIOD); - timerEventSource.cancelOnceSchedule(CustomResourceID.fromResource(customResource)); + timerEventSource.cancelOnceSchedule(ResourceID.fromResource(customResource)); untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } @@ -73,7 +73,7 @@ public void deRegistersOnceEventSources() { timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource - .cleanupForCustomResource(CustomResourceID.fromResource(customResource)); + .cleanupForResource(ResourceID.fromResource(customResource)); untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java index 10b76b8cdc..ec0e9bffbd 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java @@ -103,7 +103,7 @@ public UpdateControl reconcile( } resource.getStatus().setConfigMapStatus("ConfigMap Ready"); } - return UpdateControl.updateCustomResource(resource); + return UpdateControl.updateResource(resource); } private Map configMapData(TestCustomResource resource) { diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index ed19ed3ac2..9a44885a47 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -159,7 +159,7 @@ protected void before(ExtensionContext context) { for (var ref : controllers) { final var config = configurationService.getConfigurationFor(ref.controller); final var oconfig = override(config).settingNamespace(namespace); - final var path = "/META-INF/fabric8/" + config.getCRDName() + "-v1.yml"; + final var path = "/META-INF/fabric8/" + config.getResourceTypeName() + "-v1.yml"; if (ref.retry != null) { oconfig.withRetry(ref.retry); @@ -172,7 +172,7 @@ protected void before(ExtensionContext context) { // when the operator started. This seems to be fixing this issue (maybe a problem with // minikube?) Thread.sleep(2000); - LOGGER.debug("Applied CRD with name: {}", config.getCRDName()); + LOGGER.debug("Applied CRD with name: {}", config.getResourceTypeName()); } catch (Exception ex) { throw new IllegalStateException("Cannot apply CRD yaml: " + path, ex); } @@ -199,7 +199,7 @@ protected void after(ExtensionContext context) { LOGGER.info("Waiting for namespace {} to be deleted", namespace); Awaitility.await("namespace deleted") .pollInterval(50, TimeUnit.MILLISECONDS) - .atMost(60, TimeUnit.SECONDS) + .atMost(90, TimeUnit.SECONDS) .until(() -> kubernetesClient.namespaces().withName(namespace).get() == null); } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 4539873955..a56ee66cb0 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -3,15 +3,15 @@ import java.util.Set; import java.util.function.Function; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ControllerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilters; -public class AnnotationConfiguration> +public class AnnotationConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { private final Reconciler reconciler; @@ -31,7 +31,7 @@ public String getName() { @Override public String getFinalizer() { if (annotation == null || annotation.finalizerName().isBlank()) { - return ControllerUtils.getDefaultFinalizerName(getCRDName()); + return ControllerUtils.getDefaultFinalizerName(getResourceTypeName()); } else { return annotation.finalizerName(); } @@ -44,8 +44,8 @@ public boolean isGenerationAware() { } @Override - public Class getCustomResourceClass() { - return RuntimeControllerMetadata.getCustomResourceClass(reconciler); + public Class getResourceClass() { + return RuntimeControllerMetadata.getResourceClass(reconciler); } @Override @@ -75,17 +75,17 @@ public String getAssociatedReconcilerClassName() { @SuppressWarnings("unchecked") @Override - public CustomResourceEventFilter getEventFilter() { - CustomResourceEventFilter answer = null; + public ResourceEventFilter getEventFilter() { + ResourceEventFilter answer = null; - Class>[] filterTypes = - (Class>[]) valueOrDefault(annotation, + Class>[] filterTypes = + (Class>[]) valueOrDefault(annotation, ControllerConfiguration::eventFilters, new Object[] {}); if (filterTypes.length > 0) { for (var filterType : filterTypes) { try { - CustomResourceEventFilter filter = filterType.getConstructor().newInstance(); + ResourceEventFilter filter = filterType.getConstructor().newInstance(); if (answer == null) { answer = filter; @@ -99,7 +99,7 @@ public CustomResourceEventFilter getEventFilter() { } return answer != null ? answer - : CustomResourceEventFilters.passthrough(); + : ResourceEventFilters.passthrough(); } public static T valueOrDefault(ControllerConfiguration controllerConfiguration, diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java index a0fadbb8cb..f2f0167de0 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java @@ -24,8 +24,8 @@ static Map provide(final String resourcePath, T key, V value) { Map result = new HashMap<>(); try { final var classLoader = Thread.currentThread().getContextClassLoader(); - final Enumeration customResourcesMetadataList = classLoader.getResources(resourcePath); - for (Iterator it = customResourcesMetadataList.asIterator(); it.hasNext();) { + final Enumeration resourcesMetadataList = classLoader.getResources(resourcePath); + for (Iterator it = resourcesMetadataList.asIterator(); it.hasNext();) { URL url = it.next(); List classNamePairs = retrieveClassNamePairs(url); @@ -47,7 +47,7 @@ static Map provide(final String resourcePath, T key, V value) { } }); } - log.debug("Loaded Controller to CustomResource mappings {}", result); + log.debug("Loaded Controller to resource mappings {}", result); return result; } catch (IOException e) { throw new RuntimeException(e); 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 bcd2ed2f59..78cb631d63 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 @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator.config.runtime; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Utils; @@ -19,12 +19,12 @@ public static DefaultConfigurationService instance() { } @Override - public > ControllerConfiguration getConfigurationFor( + public ControllerConfiguration getConfigurationFor( Reconciler reconciler) { return getConfigurationFor(reconciler, true); } - > ControllerConfiguration getConfigurationFor( + ControllerConfiguration getConfigurationFor( Reconciler reconciler, boolean createIfNeeded) { var config = super.getConfigurationFor(reconciler); if (config == null) { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java index 68ba58f1c1..1595f88f97 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java @@ -2,31 +2,31 @@ import java.util.Map; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @SuppressWarnings("rawtypes") public class RuntimeControllerMetadata { public static final String RECONCILERS_RESOURCE_PATH = "javaoperatorsdk/reconcilers"; - private static final Map, Class> controllerToCustomResourceMappings; + private static final Map, Class> controllerToCustomResourceMappings; static { controllerToCustomResourceMappings = ClassMappingProvider.provide( - RECONCILERS_RESOURCE_PATH, Reconciler.class, CustomResource.class); + RECONCILERS_RESOURCE_PATH, Reconciler.class, HasMetadata.class); } - static > Class getCustomResourceClass( + static Class getResourceClass( Reconciler reconciler) { - final Class customResourceClass = + final Class resourceClass = controllerToCustomResourceMappings.get(reconciler.getClass()); - if (customResourceClass == null) { + if (resourceClass == null) { throw new IllegalArgumentException( String.format( "No custom resource has been found for controller %s", reconciler.getClass().getCanonicalName())); } - return (Class) customResourceClass; + return (Class) resourceClass; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 06a1238bb5..07a9348191 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -40,7 +40,7 @@ public void testUsingInformerToWatchChangesOfConfigMap() { waitForCRStatusValue(INITIAL_STATUS_MESSAGE); configMap.getData().put(TARGET_CONFIG_MAP_KEY, UPDATE_STATUS_MESSAGE); - configMap = operator.replace(ConfigMap.class, configMap); + operator.replace(ConfigMap.class, configMap); waitForCRStatusValue(UPDATE_STATUS_MESSAGE); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java new file mode 100644 index 0000000000..252e7f603a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java @@ -0,0 +1,72 @@ +package io.javaoperatorsdk.operator; + +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.deployment.DeploymentReconciler; + +import static io.javaoperatorsdk.operator.sample.deployment.DeploymentReconciler.STATUS_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class KubernetesResourceStatusUpdateIT { + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new DeploymentReconciler()) + .build(); + + @Test + public void testReconciliationOfNonCustomResourceAndStatusUpdate() { + var deployment = operator.create(Deployment.class, testDeployment()); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + var d = operator.get(Deployment.class, deployment.getMetadata().getName()); + assertThat(d.getStatus()).isNotNull(); + assertThat(d.getStatus().getConditions()).isNotNull(); + assertThat( + d.getStatus().getConditions().stream().filter(c -> c.getMessage().equals(STATUS_MESSAGE)) + .count()).isEqualTo(1); + }); + } + + private Deployment testDeployment() { + Deployment resource = new Deployment(); + resource.setMetadata( + new ObjectMetaBuilder() + .withName("test-deployment") + .build()); + DeploymentSpec spec = new DeploymentSpec(); + resource.setSpec(spec); + spec.setReplicas(1); + var labelSelector = new HashMap(); + labelSelector.put("app", "nginx"); + spec.setSelector(new LabelSelector(null, labelSelector)); + PodTemplateSpec podTemplate = new PodTemplateSpec(); + spec.setTemplate(podTemplate); + + podTemplate.setMetadata(new ObjectMeta()); + podTemplate.getMetadata().setLabels(labelSelector); + podTemplate.setSpec(new PodSpec()); + + Container container = new Container(); + container.setName("nginx"); + container.setImage("nginx:1.14.2"); + ContainerPort port = new ContainerPort(); + port.setContainerPort(80); + container.setPorts(List.of(port)); + + podTemplate.getSpec().setContainers(List.of(container)); + return resource; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 425f822ddc..fa1101dbd4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -78,11 +78,11 @@ public void returnsValuesFromControllerAnnotationFinalizer() { final var configuration = DefaultConfigurationService.instance().getConfigurationFor(reconciler); assertEquals(CustomResource.getCRDName(TestCustomResource.class), - configuration.getCRDName()); + configuration.getResourceTypeName()); assertEquals( - ControllerUtils.getDefaultFinalizerName(configuration.getCRDName()), + ControllerUtils.getDefaultFinalizerName(configuration.getResourceTypeName()), configuration.getFinalizer()); - assertEquals(TestCustomResource.class, configuration.getCustomResourceClass()); + assertEquals(TestCustomResource.class, configuration.getResourceClass()); assertFalse(configuration.isGenerationAware()); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java new file mode 100644 index 0000000000..a7340465ef --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java @@ -0,0 +1,56 @@ +package io.javaoperatorsdk.operator.sample.deployment; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentCondition; +import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; +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.support.TestExecutionInfoProvider; + +@ControllerConfiguration +public class DeploymentReconciler + implements Reconciler, TestExecutionInfoProvider { + + public static final String STATUS_MESSAGE = "Reconciled by DeploymentReconciler"; + + private static final Logger log = LoggerFactory.getLogger(DeploymentReconciler.class); + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + Deployment resource, Context context) { + + log.info("Reconcile deployment: {}", resource); + numberOfExecutions.incrementAndGet(); + if (resource.getStatus() == null) { + resource.setStatus(new DeploymentStatus()); + } + if (resource.getStatus().getConditions() == null) { + resource.getStatus().setConditions(new ArrayList<>()); + } + var conditions = resource.getStatus().getConditions(); + var condition = + conditions.stream().filter(c -> c.getMessage().equals(STATUS_MESSAGE)).findFirst(); + if (condition.isEmpty()) { + conditions.add(new DeploymentCondition(null, null, STATUS_MESSAGE, null, + "unknown", "DeploymentReconciler")); + return UpdateControl.updateStatusSubResource(resource); + } else { + return UpdateControl.noUpdate(); + } + } + + + @Override + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index 7ad8f20bae..3a922ce092 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -8,7 +8,9 @@ import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -@ControllerConfiguration +import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) public class ErrorStatusHandlerTestReconciler implements Reconciler, TestExecutionInfoProvider, ErrorStatusHandler { diff --git a/operator-framework/src/test/resources/compile-fixtures/MultilevelReconciler.java b/operator-framework/src/test/resources/compile-fixtures/MultilevelReconciler.java index fe15e54a6c..acea0a0db2 100644 --- a/operator-framework/src/test/resources/compile-fixtures/MultilevelReconciler.java +++ b/operator-framework/src/test/resources/compile-fixtures/MultilevelReconciler.java @@ -17,7 +17,7 @@ public static class MyCustomResource extends CustomResource { public UpdateControl reconcile( MultilevelReconciler.MyCustomResource customResource, Context context) { - return UpdateControl.updateCustomResource(null); + return UpdateControl.updateResource(null); } public DeleteControl cleanup(MultilevelReconciler.MyCustomResource customResource, diff --git a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java index 6331c54d2c..5ab0596319 100644 --- a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java +++ b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java @@ -16,7 +16,7 @@ public static class MyCustomResource extends CustomResource { @Override public UpdateControl reconcile(MyCustomResource customResource, Context context) { - return UpdateControl.updateCustomResource(null); + return UpdateControl.updateResource(null); } @Override diff --git a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java index 17280051a2..b95495a614 100644 --- a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java +++ b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplementedIntermediateAbstractClass.java @@ -13,7 +13,7 @@ public class ReconcilerImplementedIntermediateAbstractClass extends public UpdateControl reconcile( AbstractReconciler.MyCustomResource customResource, Context context) { - return UpdateControl.updateCustomResource(null); + return UpdateControl.updateResource(null); } public DeleteControl cleanup(AbstractReconciler.MyCustomResource customResource, diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 1aa7f1cfd0..62afafb365 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -17,10 +17,9 @@ import io.fabric8.kubernetes.client.dsl.ServiceResource; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; -import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; import static java.util.Collections.EMPTY_SET; @@ -52,7 +51,7 @@ public void prepareEventSources(EventSourceRegistry eventSourceRegistry) this.informerEventSource = new InformerEventSource<>(deploymentInformer, d -> { var ownerReferences = d.getMetadata().getOwnerReferences(); if (!ownerReferences.isEmpty()) { - return Set.of(new CustomResourceID(ownerReferences.get(0).getName(), + return Set.of(new ResourceID(ownerReferences.get(0).getName(), d.getMetadata().getNamespace())); } else { return EMPTY_SET; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 64f5db8e77..017a4a79b1 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -17,11 +17,10 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; -import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.CustomResourceID; import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.internal.CustomResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.internal.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; import okhttp3.Response; @@ -46,13 +45,13 @@ public void prepareEventSources(EventSourceRegistry eventSourceRegistry) // To find the related customResourceId of the WebApp resource we traverse the cache to // and identify it based on naming convention. var webAppInformer = - eventSourceRegistry.getCustomResourceEventSource() - .getInformer(CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY); + eventSourceRegistry.getControllerResourceEventSource() + .getInformer(ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY); var ids = webAppInformer.getStore().list().stream() .filter( (Webapp webApp) -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) - .map(webapp -> new CustomResourceID(webapp.getMetadata().getName(), + .map(webapp -> new ResourceID(webapp.getMetadata().getName(), webapp.getMetadata().getNamespace())) .collect(Collectors.toSet()); return ids; diff --git a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java index 5400a8bef3..60d859565e 100644 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java +++ b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java @@ -57,6 +57,6 @@ public UpdateControl reconcile( .endMetadata() .withSpec(serviceSpec) .build()); - return UpdateControl.updateCustomResource(resource); + return UpdateControl.updateResource(resource); } } From d842c1f426a49cf1bbbd325d66af01232cd83c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 22 Nov 2021 15:57:10 +0100 Subject: [PATCH 0137/1608] refactor: API changes of Update Control (#694) --- .../operator/api/reconciler/UpdateControl.java | 4 ++-- .../operator/processing/ReconciliationDispatcherTest.java | 8 ++++---- .../event/internal/CustomResourceSelectorTest.java | 2 +- .../operator/sample/deployment/DeploymentReconciler.java | 2 +- .../doubleupdate/DoubleUpdateTestCustomReconciler.java | 2 +- .../sample/event/EventSourceTestCustomReconciler.java | 2 +- .../InformerEventSourceTestCustomReconciler.java | 2 +- .../operator/sample/retry/RetryTestCustomReconciler.java | 2 +- .../operator/sample/simple/TestReconciler.java | 2 +- .../subresource/SubResourceTestCustomReconciler.java | 2 +- .../javaoperatorsdk/operator/sample/TomcatReconciler.java | 2 +- .../javaoperatorsdk/operator/sample/WebappReconciler.java | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index 3646a6df0f..eff9b68055 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -23,7 +23,7 @@ public static UpdateControl updateResource(T customRe return new UpdateControl<>(customResource, false, true); } - public static UpdateControl updateStatusSubResource( + public static UpdateControl updateStatus( T customResource) { return new UpdateControl<>(customResource, true, false); } @@ -35,7 +35,7 @@ public static UpdateControl updateStatusSubResource( * @param customResource - custom resource to use in both API calls * @return UpdateControl instance */ - public static UpdateControl updateCustomResourceAndStatus( + public static UpdateControl updateResourceAndStatus( T customResource) { return new UpdateControl<>(customResource, true, true); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java index 052ac91a0e..983a4fad06 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java @@ -92,7 +92,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateStatusSubResource(testCustomResource)); + .thenReturn(UpdateControl.updateStatus(testCustomResource)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -105,7 +105,7 @@ void updatesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateCustomResourceAndStatus(testCustomResource)); + .thenReturn(UpdateControl.updateResourceAndStatus(testCustomResource)); when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -281,7 +281,7 @@ void setReScheduleToPostExecutionControlFromUpdateControl() { when(reconciler.reconcile(eq(testCustomResource), any())) .thenReturn( - UpdateControl.updateStatusSubResource(testCustomResource).rescheduleAfter(1000L)); + UpdateControl.updateStatus(testCustomResource).rescheduleAfter(1000L)); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -315,7 +315,7 @@ void setObservedGenerationForStatusIfNeeded() { when(lConfiguration.isGenerationAware()).thenReturn(true); when(lController.reconcile(eq(observedGenResource), any())) - .thenReturn(UpdateControl.updateStatusSubResource(observedGenResource)); + .thenReturn(UpdateControl.updateStatus(observedGenResource)); when(lFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource); PostExecutionControl control = lDispatcher.handleExecution( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java index ad741aa85d..14218b9398 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java @@ -173,7 +173,7 @@ public UpdateControl reconcile( consumer.accept(resource); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java index a7340465ef..6a9bd352ce 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java @@ -42,7 +42,7 @@ public UpdateControl reconcile( if (condition.isEmpty()) { conditions.add(new DeploymentCondition(null, null, STATUS_MESSAGE, null, "unknown", "DeploymentReconciler")); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } else { return UpdateControl.noUpdate(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java index 2f65be15a6..bd398d0a1c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java @@ -34,7 +34,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(DoubleUpdateTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateCustomResourceAndStatus(resource); + return UpdateControl.updateResourceAndStatus(resource); } private void ensureStatusExists(DoubleUpdateTestCustomResource resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java index c8a5c673c0..7ae7e75fe3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java @@ -27,7 +27,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(EventSourceTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateStatusSubResource(resource).rescheduleAfter(TIMER_PERIOD); + return UpdateControl.updateStatus(resource).rescheduleAfter(TIMER_PERIOD); } private void ensureStatusExists(EventSourceTestCustomResource resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 322ff6194e..242498575a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -55,7 +55,7 @@ public UpdateControl reconcile( LOGGER.debug("Setting target status for CR: {}", targetStatus); resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); resource.getStatus().setConfigMapValue(targetStatus); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index 5ae059342e..b2a7ca0e17 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -46,7 +46,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(RetryTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } private void ensureStatusExists(RetryTestCustomResource resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index 30f57fd8c6..06efcf8cdd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -128,7 +128,7 @@ public UpdateControl reconcile( } resource.getStatus().setConfigMapStatus("ConfigMap Ready"); } - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } private Map configMapData(TestCustomResource resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index 9eefc7568d..c9e68e0d69 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -36,7 +36,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(SubResourceTestCustomResourceStatus.State.SUCCESS); - return UpdateControl.updateStatusSubResource(resource); + return UpdateControl.updateStatus(resource); } private void ensureStatusExists(SubResourceTestCustomResource resource) { diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 62afafb365..65e4030118 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -75,7 +75,7 @@ public UpdateControl reconcile(Tomcat tomcat, Context context) { tomcat.getMetadata().getName(), tomcat.getMetadata().getNamespace(), tomcat.getStatus().getReadyReplicas()); - return UpdateControl.updateStatusSubResource(updatedTomcat); + return UpdateControl.updateStatus(updatedTomcat); } return UpdateControl.noUpdate(); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 017a4a79b1..9b9b566061 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -98,7 +98,7 @@ public UpdateControl reconcile(Webapp webapp, Context context) { } webapp.getStatus().setDeployedArtifact(webapp.getSpec().getUrl()); webapp.getStatus().setDeploymentStatus(commandStatusInAllPods); - return UpdateControl.updateStatusSubResource(webapp); + return UpdateControl.updateStatus(webapp); } else { log.info("WebappController invoked but Tomcat not ready yet ({}/{})", tomcat.getStatus() != null ? tomcat.getStatus().getReadyReplicas() : 0, From 319ebee6ba9de1c734756a0e74e7cb6c57177a46 Mon Sep 17 00:00:00 2001 From: heesuk-ahn <42938941+heesuk-ahn@users.noreply.github.com> Date: Tue, 23 Nov 2021 17:45:07 +0900 Subject: [PATCH 0138/1608] [issue-687] Refactor/operator register api (#688) --- .../io/javaoperatorsdk/operator/Operator.java | 50 ++++++------ .../operator/OperatorTest.java | 79 +++++++++++++++++++ 2 files changed, 102 insertions(+), 27 deletions(-) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java 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 0ec41a495c..2c14f4da8c 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 @@ -70,7 +70,7 @@ public void start() { log.info("Client version: {}", Version.clientVersion()); try { - final var k8sVersion = kubernetesClient.getVersion(); + final var k8sVersion = kubernetesClient.getKubernetesVersion(); if (k8sVersion != null) { log.info("Server version: {}.{}", k8sVersion.getMajor(), k8sVersion.getMinor()); } @@ -110,13 +110,14 @@ public void close() { * Add a registration requests for the specified controller with this operator. The effective * registration of the controller is delayed till the operator is started. * - * @param controller the controller to register + * @param reconciler the controller to register * @param the {@code CustomResource} type associated with the controller * @throws OperatorException if a problem occurred during the registration process */ - public void register(Reconciler controller) + public void register(Reconciler reconciler) throws OperatorException { - register(controller, null); + final var defaultConfiguration = configurationService.getConfigurationFor(reconciler); + register(reconciler, defaultConfiguration); } /** @@ -127,39 +128,34 @@ public void register(Reconciler controller) * controller is delayed till the operator is started. * * @param reconciler part of the controller to register - * @param configuration the configuration with which we want to register the controller, if {@code - * null}, the controller's original configuration is used + * @param configuration the configuration with which we want to register the controller * @param the {@code CustomResource} type associated with the controller * @throws OperatorException if a problem occurred during the registration process */ - public void register( - Reconciler reconciler, ControllerConfiguration configuration) + public void register(Reconciler reconciler, + ControllerConfiguration configuration) throws OperatorException { - final var existing = configurationService.getConfigurationFor(reconciler); - if (existing == null) { + + if (configuration == null) { throw new OperatorException( "Cannot register controller with name " + reconciler.getClass().getCanonicalName() + " controller named " + ControllerUtils.getNameFor(reconciler) + " because its configuration cannot be found.\n" + " Known controllers are: " + configurationService.getKnownControllerNames()); - } else { - if (configuration == null) { - configuration = existing; - } - final var controller = - new Controller<>(reconciler, configuration, kubernetesClient); - controllers.add(controller); - - final var watchedNS = - configuration.watchAllNamespaces() - ? "[all namespaces]" - : configuration.getEffectiveNamespaces(); - log.info( - "Registered Controller: '{}' for CRD: '{}' for namespace(s): {}", - configuration.getName(), - configuration.getResourceClass(), - watchedNS); } + + final var controller = new Controller<>(reconciler, configuration, kubernetesClient); + + controllers.add(controller); + + final var watchedNS = configuration.watchAllNamespaces() ? "[all namespaces]" + : configuration.getEffectiveNamespaces(); + + log.info( + "Registered Controller: '{}' for CRD: '{}' for namespace(s): {}", + configuration.getName(), + configuration.getResourceClass(), + watchedNS); } static class ControllerManager implements LifecycleAware { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java new file mode 100644 index 0000000000..28a19d1081 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -0,0 +1,79 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class OperatorTest { + + private final KubernetesClient kubernetesClient = mock(KubernetesClient.class); + private final ConfigurationService configurationService = mock(ConfigurationService.class); + private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); + + private final Operator operator = new Operator(kubernetesClient, configurationService); + private final FooReconciler fooReconciler = FooReconciler.create(); + + @Test + @DisplayName("should register `Reconciler` to Controller") + public void shouldRegisterReconcilerToController() { + // given + when(configurationService.getConfigurationFor(fooReconciler)).thenReturn(configuration); + when(configuration.watchAllNamespaces()).thenReturn(true); + when(configuration.getName()).thenReturn("FOO"); + when(configuration.getResourceClass()).thenReturn(FooReconciler.class); + + // when + operator.register(fooReconciler); + + // then + verify(configuration).watchAllNamespaces(); + verify(configuration).getName(); + verify(configuration).getResourceClass(); + + assertThat(operator.getControllers().size()).isEqualTo(1); + assertThat(operator.getControllers().get(0).getReconciler()).isEqualTo(fooReconciler); + } + + @Test + @DisplayName("should throw `OperationException` when Configuration is null") + public void shouldThrowOperatorExceptionWhenConfigurationIsNull() { + Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler, null)); + } + + private static class FooCustomResource extends CustomResource { + } + + private static class FooSpec { + } + + private static class FooStatus { + } + + private static class FooReconciler implements Reconciler { + + private FooReconciler() {} + + public static FooReconciler create() { + return new FooReconciler(); + } + + @Override + public UpdateControl reconcile(FooCustomResource resource, Context context) { + return UpdateControl.noUpdate(); + } + } + +} From d3e3c450f75bf4193b4a56a9d3c14fa00674c62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 23 Nov 2021 10:47:42 +0100 Subject: [PATCH 0139/1608] Add Webpage Sample to V2 + fix for observed generation (#693) --- .../operator/api/ObservedGenerationAware.java | 4 +- .../api/ObservedGenerationAwareStatus.java | 6 +- .../processing/ReconciliationDispatcher.java | 12 +- .../event/internal/ResourceEventFilters.java | 2 +- .../ReconciliationDispatcherTest.java | 2 +- .../ObservedGenerationHandlingIT.java | 40 ++++ .../ObservedGenerationTestCustomResource.java | 23 +++ ...vedGenerationTestCustomResourceStatus.java | 7 + .../ObservedGenerationTestReconciler.java | 23 +++ sample-operators/pom.xml | 55 +----- sample-operators/webpage/README.md | 53 ++++++ sample-operators/webpage/k8s/operator.yaml | 98 ++++++++++ sample-operators/webpage/k8s/webpage.yaml | 14 ++ sample-operators/webpage/pom.xml | 68 +++++++ .../sample/ErrorSimulationException.java | 8 + .../operator/sample/WebPage.java | 17 ++ .../operator/sample/WebPageOperator.java | 34 ++++ .../operator/sample/WebPageReconciler.java | 180 ++++++++++++++++++ .../operator/sample/WebPageSpec.java | 14 ++ .../operator/sample/WebPageStatus.java | 37 ++++ .../operator/sample/deployment.yaml | 26 +++ .../operator/sample/html-configmap.yaml | 6 + .../operator/sample/ingress.yaml | 19 ++ .../operator/sample/service.yaml | 12 ++ .../webpage/src/main/resources/log4j2.xml | 13 ++ 25 files changed, 704 insertions(+), 69 deletions(-) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java create mode 100644 sample-operators/webpage/README.md create mode 100644 sample-operators/webpage/k8s/operator.yaml create mode 100644 sample-operators/webpage/k8s/webpage.yaml create mode 100644 sample-operators/webpage/pom.xml create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java create mode 100644 sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml create mode 100644 sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/html-configmap.yaml create mode 100644 sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml create mode 100644 sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml create mode 100644 sample-operators/webpage/src/main/resources/log4j2.xml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java index e0a05a6b6c..79c149e366 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.api; -import java.util.Optional; - import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -24,6 +22,6 @@ public interface ObservedGenerationAware { void setObservedGeneration(Long generation); - Optional getObservedGeneration(); + Long getObservedGeneration(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java index 843736242b..d2048c9513 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.api; -import java.util.Optional; - /** * A helper base class for status sub-resources classes to extend to support generate awareness. */ @@ -15,7 +13,7 @@ public void setObservedGeneration(Long generation) { } @Override - public Optional getObservedGeneration() { - return Optional.ofNullable(observedGeneration); + public Long getObservedGeneration() { + return observedGeneration; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java index c198252678..5f5882a7b5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java @@ -75,9 +75,9 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) Context context = new DefaultContext(executionScope.getRetryInfo()); if (markedForDeletion) { - return handleDelete(resource, context); + return handleCleanup(resource, context); } else { - return handleCreateOrUpdate(executionScope, resource, context); + return handleReconcile(executionScope, resource, context); } } @@ -99,7 +99,7 @@ private boolean shouldNotDispatchToDelete(R resource) { return configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer()); } - private PostExecutionControl handleCreateOrUpdate( + private PostExecutionControl handleReconcile( ExecutionScope executionScope, R resource, Context context) { if (configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer())) { /* @@ -114,7 +114,7 @@ private PostExecutionControl handleCreateOrUpdate( try { var resourceForExecution = cloneResourceForErrorStatusHandlerIfNeeded(resource, context); - return createOrUpdateExecution(executionScope, resourceForExecution, context); + return reconcileExecution(executionScope, resourceForExecution, context); } catch (RuntimeException e) { handleLastAttemptErrorStatusHandler(resource, context, e); throw e; @@ -137,7 +137,7 @@ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context } } - private PostExecutionControl createOrUpdateExecution(ExecutionScope executionScope, + private PostExecutionControl reconcileExecution(ExecutionScope executionScope, R resource, Context context) { log.debug( "Executing createOrUpdate for resource {} with version: {} with execution scope: {}", @@ -222,7 +222,7 @@ private void updatePostExecutionControlWithReschedule( baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); } - private PostExecutionControl handleDelete(R resource, Context context) { + private PostExecutionControl handleCleanup(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java index a46a9175e6..b7d69408ec 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java @@ -34,7 +34,7 @@ public final class ResourceEventFilters { var actualGeneration = newResource.getMetadata().getGeneration(); var observedGeneration = ((ObservedGenerationAware) status) .getObservedGeneration(); - return observedGeneration.map(aLong -> actualGeneration > aLong).orElse(true); + return observedGeneration == null || actualGeneration > observedGeneration; } } return oldResource == null || !generationAware || diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java index 983a4fad06..522f6091fb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java @@ -320,7 +320,7 @@ void setObservedGenerationForStatusIfNeeded() { PostExecutionControl control = lDispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); - assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration().get()) + assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) .isEqualTo(1L); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java new file mode 100644 index 0000000000..9eac198f35 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java @@ -0,0 +1,40 @@ +package io.javaoperatorsdk.operator; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenerationTestCustomResource; +import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenerationTestReconciler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class ObservedGenerationHandlingIT { + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new ObservedGenerationTestReconciler()) + .build(); + + @Test + public void testReconciliationOfNonCustomResourceAndStatusUpdate() { + var resource = new ObservedGenerationTestCustomResource(); + resource.setMetadata(new ObjectMeta()); + resource.getMetadata().setName("observed-gen1"); + + var createdResource = operator.create(ObservedGenerationTestCustomResource.class, resource); + + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + var d = operator.get(ObservedGenerationTestCustomResource.class, + createdResource.getMetadata().getName()); + assertThat(d.getStatus().getObservedGeneration()).isNotNull(); + assertThat(d.getStatus().getObservedGeneration()).isEqualTo(1); + }); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResource.java new file mode 100644 index 0000000000..51e4a1113a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResource.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("ObservedGenerationTestCustomResource") +@ShortNames("og") +public class ObservedGenerationTestCustomResource + extends CustomResource + implements Namespaced { + + @Override + protected ObservedGenerationTestCustomResourceStatus initStatus() { + return new ObservedGenerationTestCustomResourceStatus(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResourceStatus.java new file mode 100644 index 0000000000..14071775f3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestCustomResourceStatus.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class ObservedGenerationTestCustomResourceStatus extends ObservedGenerationAwareStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java new file mode 100644 index 0000000000..01a1705fe4 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.observedgeneration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.api.reconciler.*; + +import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class ObservedGenerationTestReconciler + implements Reconciler { + + private static final Logger log = LoggerFactory.getLogger(ObservedGenerationTestReconciler.class); + + @Override + public UpdateControl reconcile( + ObservedGenerationTestCustomResource resource, Context context) { + log.info("Reconcile ObservedGenerationTestCustomResource: {}", + resource.getMetadata().getName()); + return UpdateControl.updateStatus(resource); + } +} diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 289a2889c3..5d9a497cec 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -20,59 +20,6 @@ tomcat-operator + webpage - - - - io.javaoperatorsdk - operator-framework - 1.9.2 - - - org.apache.logging.log4j - log4j-slf4j-impl - 2.13.3 - - - org.takes - takes - 1.19 - - - junit - junit - 4.13.1 - test - - - org.awaitility - awaitility - 4.1.0 - test - - - - - - - com.google.cloud.tools - jib-maven-plugin - ${jib-maven-plugin.version} - - - gcr.io/distroless/java:11 - - - tomcat-operator - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - - - \ No newline at end of file diff --git a/sample-operators/webpage/README.md b/sample-operators/webpage/README.md new file mode 100644 index 0000000000..ba17b7d962 --- /dev/null +++ b/sample-operators/webpage/README.md @@ -0,0 +1,53 @@ +# WebServer Operator + +This is a simple example of how a Custom Resource backed by an Operator can serve as +an abstraction layer. This Operator will use a webserver resource, which mainly contains a +static webpage definition and creates an NGINX Deployment backed by a ConfigMap which holds +the HTML. + +This is an example input: +```yaml +apiVersion: "sample.javaoperatorsdk/v1" +kind: WebPage +metadata: + name: mynginx-hello +spec: + html: | + + + Webserver Operator + + + Hello World! + + +``` + +### Try + +The quickest way to try the operator is to run it on your local machine, while it connects to a local or remote +Kubernetes cluster. When you start it, it will use the current kubectl context on your machine to connect to the cluster. + +Before you run it you have to install the CRD on your cluster by running `kubectl apply -f k8s/crd.yaml` + +When the Operator is running you can create some Webserver Custom Resources. You can find a sample custom resource in +`k8s/webpage.yaml`. You can create it by running `kubectl apply -f k8s/webpage.yaml` + +After the Operator has picked up the new webserver resource (see the logs) it should create the NGINX server in the +same namespace where the webserver resource is created. To connect to the server using your browser you can +run `kubectl get service` and view the service created by the Operator. It should have a NodePort configured. If you are +running a single-node cluster (e.g. Docker for Mac or Minikube) you can connect to the VM on this port to access the +page. Otherwise you can change the service to a LoadBalancer (e.g on a public cloud). + +You can also try to change the HTML code in `k8s/webpage.yaml` and do another `kubectl apply -f k8s/webpage.yaml`. +This should update the actual NGINX deployment with the new configuration. + +### Build + +You can build the sample using `mvn jib:dockerBuild` this will produce a Docker image you can push to the registry +of your choice. The JAR file is built using your local Maven and JDK and then copied into the Docker image. + +### Deployment + +1. Deploy the CRD: `kubectl apply -f k8s/crd.yaml` +2. Deploy the operator: `kubectl apply -f k8s/operator.yaml` diff --git a/sample-operators/webpage/k8s/operator.yaml b/sample-operators/webpage/k8s/operator.yaml new file mode 100644 index 0000000000..926b2c31e2 --- /dev/null +++ b/sample-operators/webpage/k8s/operator.yaml @@ -0,0 +1,98 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: webserver-operator + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: webserver-operator + namespace: webserver-operator + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webserver-operator + namespace: webserver-operator +spec: + selector: + matchLabels: + app: webserver-operator + replicas: 1 + template: + metadata: + labels: + app: webserver-operator + spec: + serviceAccountName: webserver-operator + containers: + - name: operator + image: webserver-operator + imagePullPolicy: Never + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 1 + timeoutSeconds: 1 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + timeoutSeconds: 1 + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: operator-admin +subjects: +- kind: ServiceAccount + name: webserver-operator + namespace: webserver-operator +roleRef: + kind: ClusterRole + name: webserver-operator + apiGroup: "" + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: webserver-operator +rules: +- apiGroups: + - "" + resources: + - deployments + - services + - configmaps + - pods + verbs: + - '*' +- apiGroups: + - "apps" + resources: + - deployments + - services + - configmaps + verbs: + - '*' +- apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - '*' +- apiGroups: + - "sample.javaoperatorsdk" + resources: + - webservers + - webservers/status + verbs: + - '*' diff --git a/sample-operators/webpage/k8s/webpage.yaml b/sample-operators/webpage/k8s/webpage.yaml new file mode 100644 index 0000000000..382da972cb --- /dev/null +++ b/sample-operators/webpage/k8s/webpage.yaml @@ -0,0 +1,14 @@ +apiVersion: "sample.javaoperatorsdk/v1" +kind: WebPage +metadata: + name: hellows +spec: + html: | + + + Hello Operator World + + + Hello World! + + diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml new file mode 100644 index 0000000000..ed4bcafc0a --- /dev/null +++ b/sample-operators/webpage/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + io.javaoperatorsdk + sample-operators + 2.0.0-SNAPSHOT + + + webserver + Operator SDK - Samples - Webserver + Provisions an nginx Webserver based on a CRD + jar + + + 11 + 11 + 2.7.1 + + + + + io.javaoperatorsdk + operator-framework + 2.0.0-SNAPSHOT + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.takes + takes + 1.19 + + + io.fabric8 + crd-generator-apt + provided + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/distroless/java:11 + + + webserver-operator + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + + \ No newline at end of file diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java new file mode 100644 index 0000000000..e2d3f3c1dd --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.sample; + +public class ErrorSimulationException extends RuntimeException { + + public ErrorSimulationException(String message) { + super(message); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java new file mode 100644 index 0000000000..6c10ffe3af --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample; + +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.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +public class WebPage extends CustomResource + implements Namespaced { + + @Override + protected WebPageStatus initStatus() { + return new WebPageStatus(); + } +} 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 new file mode 100644 index 0000000000..4940119ba9 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; + +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.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; + +public class WebPageOperator { + + private static final Logger log = LoggerFactory.getLogger(WebPageOperator.class); + + public static void main(String[] args) throws IOException { + log.info("WebServer Operator starting!"); + + Config config = new ConfigBuilder().withNamespace(null).build(); + KubernetesClient client = new DefaultKubernetesClient(config); + Operator operator = new Operator(client, DefaultConfigurationService.instance()); + operator.register(new WebPageReconciler(client)); + operator.start(); + + new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java new file mode 100644 index 0000000000..37caf1503c --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -0,0 +1,180 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.dsl.RollableScalableResource; +import io.fabric8.kubernetes.client.dsl.ServiceResource; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@ControllerConfiguration +public class WebPageReconciler implements Reconciler, ErrorStatusHandler { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final KubernetesClient kubernetesClient; + + public WebPageReconciler(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) { + if (webPage.getSpec().getHtml().contains("error")) { + throw new ErrorSimulationException("Simulating error"); + } + + String ns = webPage.getMetadata().getNamespace(); + + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + + ConfigMap htmlConfigMap = + new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName(webPage)) + .withNamespace(ns) + .build()) + .withData(data) + .build(); + + Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + deployment.getMetadata().setName(deploymentName(webPage)); + deployment.getMetadata().setNamespace(ns); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName(webPage)); + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName(webPage)); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + + Service service = loadYaml(Service.class, "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(ns); + service.getSpec().setSelector(deployment.getSpec().getTemplate().getMetadata().getLabels()); + + ConfigMap existingConfigMap = + kubernetesClient + .configMaps() + .inNamespace(htmlConfigMap.getMetadata().getNamespace()) + .withName(htmlConfigMap.getMetadata().getName()) + .get(); + + log.info("Creating or updating ConfigMap {} in {}", htmlConfigMap.getMetadata().getName(), ns); + kubernetesClient.configMaps().inNamespace(ns).createOrReplace(htmlConfigMap); + log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); + kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(deployment); + + if (kubernetesClient.services().inNamespace(ns).withName(service.getMetadata().getName()) + .get() == null) { + log.info("Creating Service {} in {}", service.getMetadata().getName(), ns); + kubernetesClient.services().inNamespace(ns).createOrReplace(service); + } + + if (existingConfigMap != null) { + if (!StringUtils.equals( + existingConfigMap.getData().get("index.html"), + htmlConfigMap.getData().get("index.html"))) { + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(webPage)) + .delete(); + } + } + + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(htmlConfigMap.getMetadata().getName()); + status.setAreWeGood("Yes!"); + status.setErrorMessage(null); + webPage.setStatus(status); + + return UpdateControl.updateStatus(webPage); + } + + @Override + public DeleteControl cleanup(WebPage nginx, Context context) { + log.info("Execution deleteResource for: {}", nginx.getMetadata().getName()); + + log.info("Deleting ConfigMap {}", configMapName(nginx)); + Resource configMap = + kubernetesClient + .configMaps() + .inNamespace(nginx.getMetadata().getNamespace()) + .withName(configMapName(nginx)); + if (configMap.get() != null) { + configMap.delete(); + } + + log.info("Deleting Deployment {}", deploymentName(nginx)); + RollableScalableResource deployment = + kubernetesClient + .apps() + .deployments() + .inNamespace(nginx.getMetadata().getNamespace()) + .withName(deploymentName(nginx)); + if (deployment.get() != null) { + deployment.cascading(true).delete(); + } + + log.info("Deleting Service {}", serviceName(nginx)); + ServiceResource service = + kubernetesClient + .services() + .inNamespace(nginx.getMetadata().getNamespace()) + .withName(serviceName(nginx)); + if (service.get() != null) { + service.delete(); + } + return DeleteControl.defaultDelete(); + } + + private static String configMapName(WebPage nginx) { + return nginx.getMetadata().getName() + "-html"; + } + + private static String deploymentName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private static String serviceName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private T loadYaml(Class clazz, String yaml) { + try (InputStream is = getClass().getResourceAsStream(yaml)) { + return Serialization.unmarshal(is, clazz); + } catch (IOException ex) { + throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); + } + } + + @Override + public WebPage updateErrorStatus(WebPage resource, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return resource; + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java new file mode 100644 index 0000000000..db50be6c2d --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample; + +public class WebPageSpec { + + private String html; + + public String getHtml() { + return html; + } + + public void setHtml(String html) { + this.html = html; + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java new file mode 100644 index 0000000000..2b2e2a23c8 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java @@ -0,0 +1,37 @@ +package io.javaoperatorsdk.operator.sample; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class WebPageStatus extends ObservedGenerationAwareStatus { + + private String htmlConfigMap; + + private String areWeGood; + + private String errorMessage; + + public String getHtmlConfigMap() { + return htmlConfigMap; + } + + public void setHtmlConfigMap(String htmlConfigMap) { + this.htmlConfigMap = htmlConfigMap; + } + + public String getAreWeGood() { + return areWeGood; + } + + public void setAreWeGood(String areWeGood) { + this.areWeGood = areWeGood; + } + + public String getErrorMessage() { + return errorMessage; + } + + public WebPageStatus setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; + } +} diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml new file mode 100644 index 0000000000..f2d10d4325 --- /dev/null +++ b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 +kind: Deployment +metadata: + name: "" +spec: + selector: + matchLabels: + app: "" + replicas: 1 + template: + metadata: + labels: + app: "" + spec: + containers: + - name: nginx + image: nginx:1.17.0 + ports: + - containerPort: 80 + volumeMounts: + - name: html-volume + mountPath: /usr/share/nginx/html + volumes: + - name: html-volume + configMap: + name: "" \ No newline at end of file diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/html-configmap.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/html-configmap.yaml new file mode 100644 index 0000000000..8314c5b927 --- /dev/null +++ b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/html-configmap.yaml @@ -0,0 +1,6 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: "" +data: + html: "" \ No newline at end of file diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml new file mode 100644 index 0000000000..6fd1ed93e9 --- /dev/null +++ b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 +kind: Deployment +metadata: + name: +spec: + selector: + matchLabels: + app: + replicas: 1 + template: + metadata: + labels: + app: + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml new file mode 100644 index 0000000000..578fcd6d1e --- /dev/null +++ b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: "" +spec: + selector: + app: "" + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: NodePort \ No newline at end of file diff --git a/sample-operators/webpage/src/main/resources/log4j2.xml b/sample-operators/webpage/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..5b794e7de3 --- /dev/null +++ b/sample-operators/webpage/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file From a047bb92ff77e5786d77ecb3af641cdd1316f165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 24 Nov 2021 09:28:50 +0100 Subject: [PATCH 0140/1608] fix: integration test stability (#698) --- .../operator/KubernetesResourceStatusUpdateIT.java | 11 +++++++++-- .../sample/deployment/DeploymentReconciler.java | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java index 252e7f603a..2c5ea1cba1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -30,10 +31,13 @@ public class KubernetesResourceStatusUpdateIT { @Test public void testReconciliationOfNonCustomResourceAndStatusUpdate() { var deployment = operator.create(Deployment.class, testDeployment()); - await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(120, TimeUnit.SECONDS).untilAsserted(() -> { var d = operator.get(Deployment.class, deployment.getMetadata().getName()); assertThat(d.getStatus()).isNotNull(); assertThat(d.getStatus().getConditions()).isNotNull(); + // wait until the pod is ready, if not this is causing some test stability issues with + // namespace cleanup in k8s version 1.22 + assertThat(d.getStatus().getReadyReplicas()).isGreaterThanOrEqualTo(1); assertThat( d.getStatus().getConditions().stream().filter(c -> c.getMessage().equals(STATUS_MESSAGE)) .count()).isEqualTo(1); @@ -42,9 +46,12 @@ public void testReconciliationOfNonCustomResourceAndStatusUpdate() { private Deployment testDeployment() { Deployment resource = new Deployment(); + Map labels = new HashMap<>(); + labels.put("test", "KubernetesResourceStatusUpdateIT"); resource.setMetadata( new ObjectMetaBuilder() .withName("test-deployment") + .withLabels(labels) .build()); DeploymentSpec spec = new DeploymentSpec(); resource.setSpec(spec); @@ -61,7 +68,7 @@ private Deployment testDeployment() { Container container = new Container(); container.setName("nginx"); - container.setImage("nginx:1.14.2"); + container.setImage("nginx:1.21.4"); ContainerPort port = new ContainerPort(); port.setContainerPort(80); container.setPorts(List.of(port)); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java index 6a9bd352ce..3992920884 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java @@ -15,7 +15,7 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -@ControllerConfiguration +@ControllerConfiguration(labelSelector = "test=KubernetesResourceStatusUpdateIT") public class DeploymentReconciler implements Reconciler, TestExecutionInfoProvider { From 609682e8764b185f1212ddac1e702aab90df5ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 24 Nov 2021 09:29:17 +0100 Subject: [PATCH 0141/1608] Update observed gen on no-update (#697) --- .../api/reconciler/UpdateControl.java | 20 +++--- .../operator/processing/Controller.java | 4 +- .../processing/ReconciliationDispatcher.java | 71 ++++++++++++------- .../ReconciliationDispatcherTest.java | 36 +++++++--- 4 files changed, 87 insertions(+), 44 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index eff9b68055..dcc997021f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -6,16 +6,16 @@ public class UpdateControl extends BaseControl> { private final T resource; - private final boolean updateStatusSubResource; + private final boolean updateStatus; private final boolean updateResource; private UpdateControl( - T resource, boolean updateStatusSubResource, boolean updateResource) { - if ((updateResource || updateStatusSubResource) && resource == null) { + T resource, boolean updateStatus, boolean updateResource) { + if ((updateResource || updateStatus) && resource == null) { throw new IllegalArgumentException("CustomResource cannot be null in case of update"); } this.resource = resource; - this.updateStatusSubResource = updateStatusSubResource; + this.updateStatus = updateStatus; this.updateResource = updateResource; } @@ -48,15 +48,19 @@ public T getResource() { return resource; } - public boolean isUpdateStatusSubResource() { - return updateStatusSubResource; + public boolean isUpdateStatus() { + return updateStatus; } public boolean isUpdateResource() { return updateResource; } - public boolean isUpdateCustomResourceAndStatusSubResource() { - return updateResource && updateStatusSubResource; + public boolean isNoUpdate() { + return !updateResource && !updateStatus; + } + + public boolean isUpdateResourceAndStatus() { + return updateResource && updateStatus; } } 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 45d26e5a83..ff6ace1b87 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 @@ -81,10 +81,10 @@ public String controllerName() { @Override public String successTypeName(UpdateControl result) { String successType = "cr"; - if (result.isUpdateStatusSubResource()) { + if (result.isUpdateStatus()) { successType = "status"; } - if (result.isUpdateCustomResourceAndStatusSubResource()) { + if (result.isUpdateResourceAndStatus()) { successType = "both"; } return successType; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java index 5f5882a7b5..a7d55c0ae3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java @@ -81,10 +81,6 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) } } - private ControllerConfiguration configuration() { - return controller.getConfiguration(); - } - /** * Determines whether the given resource should be dispatched to the controller's * {@link Reconciler#cleanup(HasMetadata, Context)} method @@ -100,36 +96,39 @@ private boolean shouldNotDispatchToDelete(R resource) { } private PostExecutionControl handleReconcile( - ExecutionScope executionScope, R resource, Context context) { - if (configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer())) { + ExecutionScope executionScope, R originalResource, Context context) { + if (configuration().useFinalizer() + && !originalResource.hasFinalizer(configuration().getFinalizer())) { /* * We always add the finalizer if missing and the controller is configured to use a finalizer. * We execute the controller processing only for processing the event sent as a results of the * finalizer add. This will make sure that the resources are not created before there is a * finalizer. */ - updateCustomResourceWithFinalizer(resource); + updateCustomResourceWithFinalizer(originalResource); return PostExecutionControl.onlyFinalizerAdded(); } else { try { var resourceForExecution = - cloneResourceForErrorStatusHandlerIfNeeded(resource, context); - return reconcileExecution(executionScope, resourceForExecution, context); + cloneResourceForErrorStatusHandlerIfNeeded(originalResource, context); + return reconcileExecution(executionScope, resourceForExecution, originalResource, context); } catch (RuntimeException e) { - handleLastAttemptErrorStatusHandler(resource, context, e); + handleLastAttemptErrorStatusHandler(originalResource, context, e); throw e; } } } /** - * Resource make sense only to clone for the ErrorStatusHandler. Otherwise, this operation can be - * skipped since it can be memory and time-consuming. However, it needs to be cloned since it's - * common that the custom resource is changed during an execution, and it's much cleaner to have - * to original resource in place for status update. + * Resource make sense only to clone for the ErrorStatusHandler or if the observed generation in + * status is handled automatically. Otherwise, this operation can be skipped since it can be + * memory and time-consuming. However, it needs to be cloned since it's common that the custom + * resource is changed during an execution, and it's much cleaner to have to original resource in + * place for status update. */ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { - if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context)) { + if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context) || + shouldUpdateObservedGenerationAutomatically(resource)) { return controller.getConfiguration().getConfigurationService().getResourceCloner() .clone(resource); } else { @@ -138,26 +137,29 @@ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context } private PostExecutionControl reconcileExecution(ExecutionScope executionScope, - R resource, Context context) { + R resourceForExecution, R originalResource, Context context) { log.debug( "Executing createOrUpdate for resource {} with version: {} with execution scope: {}", - getName(resource), - getVersion(resource), + getName(resourceForExecution), + getVersion(resourceForExecution), executionScope); - UpdateControl updateControl = controller.reconcile(resource, context); + UpdateControl updateControl = controller.reconcile(resourceForExecution, context); R updatedCustomResource = null; - if (updateControl.isUpdateCustomResourceAndStatusSubResource()) { + if (updateControl.isUpdateResourceAndStatus()) { updatedCustomResource = updateCustomResource(updateControl.getResource()); updateControl .getResource() .getMetadata() .setResourceVersion(updatedCustomResource.getMetadata().getResourceVersion()); updatedCustomResource = updateStatusGenerationAware(updateControl.getResource()); - } else if (updateControl.isUpdateStatusSubResource()) { + } else if (updateControl.isUpdateStatus()) { updatedCustomResource = updateStatusGenerationAware(updateControl.getResource()); } else if (updateControl.isUpdateResource()) { updatedCustomResource = updateCustomResource(updateControl.getResource()); + } else if (updateControl.isNoUpdate() + && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { + updatedCustomResource = updateStatusGenerationAware(originalResource); } return createPostExecutionControl(updatedCustomResource, updateControl); } @@ -184,14 +186,27 @@ private boolean isLastAttemptOfRetryAndErrorStatusHandlerPresent(Context context } } - private R updateStatusGenerationAware(R customResource) { - updateStatusObservedGenerationIfRequired(customResource); - return customResourceFacade.updateStatus(customResource); + private R updateStatusGenerationAware(R resource) { + updateStatusObservedGenerationIfRequired(resource); + return customResourceFacade.updateStatus(resource); + } + + private boolean shouldUpdateObservedGenerationAutomatically(R resource) { + if (controller.getConfiguration().isGenerationAware() + && resource instanceof CustomResource) { + var customResource = (CustomResource) resource; + var status = customResource.getStatus(); + // Note that if status is null we won't update the observed generation. + if (status instanceof ObservedGenerationAware) { + var observedGen = ((ObservedGenerationAware) status).getObservedGeneration(); + var currentGen = resource.getMetadata().getGeneration(); + return !currentGen.equals(observedGen); + } + } + return false; } private void updateStatusObservedGenerationIfRequired(R resource) { - // todo: change this to check for HasStatus (or similar) when - // https://github.com/fabric8io/kubernetes-client/issues/3586 is fixed if (controller.getConfiguration().isGenerationAware() && resource instanceof CustomResource) { var customResource = (CustomResource) resource; @@ -280,6 +295,10 @@ private R replace(R resource) { return customResourceFacade.replaceWithLock(resource); } + private ControllerConfiguration configuration() { + return controller.getConfiguration(); + } + // created to support unit testing static class CustomResourceFacade { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java index 522f6091fb..16e5bf4b87 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java @@ -307,18 +307,38 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { void setObservedGenerationForStatusIfNeeded() { var observedGenResource = createObservedGenCustomResource(); - Reconciler lController = mock(Reconciler.class); - ControllerConfiguration lConfiguration = + Reconciler reconciler = mock(Reconciler.class); + ControllerConfiguration config = mock(ControllerConfiguration.class); - CustomResourceFacade lFacade = mock(CustomResourceFacade.class); - var lDispatcher = init(observedGenResource, lController, lConfiguration, lFacade); + CustomResourceFacade facade = mock(CustomResourceFacade.class); + var dispatcher = init(observedGenResource, reconciler, config, facade); - when(lConfiguration.isGenerationAware()).thenReturn(true); - when(lController.reconcile(eq(observedGenResource), any())) + when(config.isGenerationAware()).thenReturn(true); + when(reconciler.reconcile(any(), any())) .thenReturn(UpdateControl.updateStatus(observedGenResource)); - when(lFacade.updateStatus(observedGenResource)).thenReturn(observedGenResource); + when(facade.updateStatus(observedGenResource)).thenReturn(observedGenResource); + + PostExecutionControl control = dispatcher.handleExecution( + executionScopeWithCREvent(observedGenResource)); + assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) + .isEqualTo(1L); + } + + @Test + void updatesObservedGenerationOnNoUpdateUpdateControl() { + var observedGenResource = createObservedGenCustomResource(); + + Reconciler reconciler = mock(Reconciler.class); + ControllerConfiguration config = + mock(ControllerConfiguration.class); + CustomResourceFacade facade = mock(CustomResourceFacade.class); + when(config.isGenerationAware()).thenReturn(true); + when(reconciler.reconcile(any(), any())) + .thenReturn(UpdateControl.noUpdate()); + when(facade.updateStatus(observedGenResource)).thenReturn(observedGenResource); + var dispatcher = init(observedGenResource, reconciler, config, facade); - PostExecutionControl control = lDispatcher.handleExecution( + PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) .isEqualTo(1L); From 65d665b6f40113432554d77a65b94fbbb2a5f173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 24 Nov 2021 13:32:06 +0100 Subject: [PATCH 0142/1608] feature: release of v1 and v2 (#701) --- .github/workflows/release.yml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a8c53bd9d..d5de5b7996 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + if: ${{ startsWith(github.event.release.tag_name, 'v1.' ) }} + with: + ref: "v1" + - uses: actions/checkout@v2 + if: ${{ startsWith(github.event.release.tag_name, 'v2.') }} - name: Set up Java and Maven uses: actions/setup-java@v2 with: @@ -20,20 +25,25 @@ jobs: run: ./mvnw ${MAVEN_ARGS} versions:set -DnewVersion="${RELEASE_VERSION:1}" versions:commit env: RELEASE_VERSION: ${{ github.event.release.tag_name }} - - name: Release Maven package - uses: samuelmeuli/action-maven-publish@v1 - with: - maven_profiles: "release" - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} - nexus_username: ${{ secrets.OSSRH_USERNAME }} - nexus_password: ${{ secrets.OSSRH_TOKEN }} +# - name: Release Maven package +# uses: samuelmeuli/action-maven-publish@v1 +# with: +# maven_profiles: "release" +# gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} +# gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} +# nexus_username: ${{ secrets.OSSRH_USERNAME }} +# nexus_password: ${{ secrets.OSSRH_TOKEN }} # This is separate job because there were issues with git after release step, was not able to commit changes. See history. update-working-version: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + if: ${{ startsWith(github.event.release.tag_name, 'v1.' ) }} + with: + ref: "v1" + - uses: actions/checkout@v2 + if: ${{ startsWith(github.event.release.tag_name, 'v2.') }} - name: Set up Java and Maven uses: actions/setup-java@v2 with: From 9769d3ab1df7e51b64e8e9e8894238a9ceda4627 Mon Sep 17 00:00:00 2001 From: csviri Date: Wed, 24 Nov 2021 13:59:12 +0100 Subject: [PATCH 0143/1608] fix: webpage naming --- sample-operators/webpage/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index ed4bcafc0a..34286abadc 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -10,9 +10,9 @@ 2.0.0-SNAPSHOT - webserver - Operator SDK - Samples - Webserver - Provisions an nginx Webserver based on a CRD + webpage + Operator SDK - Samples - WebPage + Provisions an nginx Webserver based on a CRD with give html jar From 290c6f9672dfdbc8507b989b085cb65d45cc41e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 24 Nov 2021 16:00:09 +0100 Subject: [PATCH 0144/1608] fix: build javadoc issue (#703) --- .../operator/api/ObservedGenerationAware.java | 8 ++++---- .../operator/api/reconciler/UpdateControl.java | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java index 79c149e366..370e497652 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -11,10 +12,9 @@ * is ignored. * * In order to work the status object returned by CustomResource.getStatus() should not be null. In - * addition to that from the controller that the - * {@link UpdateControl#updateStatusSubResource(CustomResource)} or - * {@link UpdateControl#updateCustomResourceAndStatus(CustomResource)} should be returned. The - * observed generation is not updated in other cases. + * addition to that from the controller that the {@link UpdateControl#updateStatus(HasMetadata)} or + * {@link UpdateControl#updateResourceAndStatus(HasMetadata)} should be returned. The observed + * generation is not updated in other cases. * * @see ObservedGenerationAwareStatus */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index dcc997021f..1affc25abd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -31,7 +31,8 @@ public static UpdateControl updateStatus( /** * As a results of this there will be two call to K8S API. First the custom resource will be * updates then the status sub-resource. - * + * + * @param resource type * @param customResource - custom resource to use in both API calls * @return UpdateControl instance */ From b5c35d5f061ecaa11e7c5c9331b8891e96bd1d05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Nov 2021 09:08:24 +0100 Subject: [PATCH 0145/1608] chore(deps): bump awaitility from 4.1.0 to 4.1.1 (#710) Bumps [awaitility](https://github.com/awaitility/awaitility) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/awaitility/awaitility/releases) - [Changelog](https://github.com/awaitility/awaitility/blob/master/changelog.txt) - [Commits](https://github.com/awaitility/awaitility/compare/awaitility-4.1.0...awaitility-4.1.1) --- updated-dependencies: - dependency-name: org.awaitility:awaitility dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c11942d447..0c2cd0bf72 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ 0.19 1.13.0 3.21.0 - 4.1.0 + 4.1.1 2.5.5 1.7.4 diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 77f87b8158..93ca257ed5 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -51,7 +51,7 @@ org.awaitility awaitility - 4.1.0 + 4.1.1 test From ea814e7c22be74680a990bf253e5d27d8ddb953f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Nov 2021 09:08:29 +0100 Subject: [PATCH 0146/1608] chore(deps): bump micrometer-core from 1.7.4 to 1.8.0 (#709) Bumps [micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.7.4 to 1.8.0. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.7.4...v1.8.0) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0c2cd0bf72..16c0c293aa 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ 3.21.0 4.1.1 2.5.5 - 1.7.4 + 1.8.0 2.11 3.8.1 From 933d2e0f73a10058180e0206ffa2a386a7f71f1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Nov 2021 09:08:32 +0100 Subject: [PATCH 0147/1608] chore(deps-dev): bump mockito-core from 4.0.0 to 4.1.0 (#708) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.0.0...v4.1.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 16c0c293aa..ce925fac06 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 5.10.1 1.7.32 2.14.1 - 4.0.0 + 4.1.0 3.12.0 1.0.1 0.19 From f02d76f9cfbc393f89d3e18743f6f88b6638ef54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Nov 2021 09:08:51 +0100 Subject: [PATCH 0148/1608] chore(deps): bump container-tools/kind-action from 1.5.0 to 1.7.0 (#706) Bumps [container-tools/kind-action](https://github.com/container-tools/kind-action) from 1.5.0 to 1.7.0. - [Release notes](https://github.com/container-tools/kind-action/releases) - [Commits](https://github.com/container-tools/kind-action/compare/v1.5.0...v1.7.0) --- updated-dependencies: - dependency-name: container-tools/kind-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/end-to-end-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index 404be8c102..634824c61b 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -24,7 +24,7 @@ jobs: sleep 1 - name: Create Kubernetes KinD Cluster - uses: container-tools/kind-action@v1.5.0 + uses: container-tools/kind-action@v1.7.0 with: cluster_name: e2e-test registry: false From 8c614a01364e088d757246409325e75e6132431d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 25 Nov 2021 09:10:10 +0100 Subject: [PATCH 0149/1608] fix: fixing maven version update (#704) --- .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 936c95fb10..63ea4ee489 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: true on: pull_request: - branches: [ master, v2 ] + branches: [ master, v1 ] workflow_dispatch: jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5de5b7996..e25b3bee29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,14 @@ jobs: git commit -m "Set new SNAPSHOT version into pom files." -a env: RELEASE_VERSION: ${{ github.event.release.tag_name }} - - name: Push changes + - name: Push changes v1 uses: ad-m/github-push-action@master + if: ${{ startsWith(github.event.release.tag_name, 'v1.' ) }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: "v1" + - name: Push changes v2 + uses: ad-m/github-push-action@master + if: ${{ startsWith(github.event.release.tag_name, 'v2.' ) }} with: github_token: ${{ secrets.GITHUB_TOKEN }} From 2f3b9b1d24c48142785b06a2d5fa60c5ae1abe47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Nov 2021 09:15:42 +0100 Subject: [PATCH 0150/1608] chore(deps): bump spring-boot.version from 2.5.5 to 2.6.0 (#707) Bumps `spring-boot.version` from 2.5.5 to 2.6.0. Updates `spring-boot-dependencies` from 2.5.5 to 2.6.0 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.5.5...v2.6.0) Updates `spring-boot-maven-plugin` from 2.5.5 to 2.6.0 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.5.5...v2.6.0) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ce925fac06..1517bd034c 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 1.13.0 3.21.0 4.1.1 - 2.5.5 + 2.6.0 1.8.0 2.11 From c6357e61fc3f01d80997eea54514a57e23f96f50 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 25 Nov 2021 08:29:11 +0000 Subject: [PATCH 0151/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 2866f42257..fc055ea220 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index af0737bf29..41ecb0f137 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 7a9977ded0..0339129c75 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 2fe67f8716..0c05632099 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 1517bd034c..809c25752c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 5d9a497cec..878c4a81df 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 93ca257ed5..49144bf646 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 34286abadc..7a6c711df5 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index ed1884aecd..0038730ea3 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 566fbd98be..2d1ad2923a 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index d1a2de4c2b..9fb7f1c92c 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index de64a684e5..cbdcabe77a 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 6f70a482ab5a8079dd3d2b8332ff31eef8f7cb11 Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 25 Nov 2021 09:50:39 +0100 Subject: [PATCH 0152/1608] fix: revert version, finalize release script --- .github/workflows/release.yml | 16 ++++++++-------- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e25b3bee29..b36caff8f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,14 +25,14 @@ jobs: run: ./mvnw ${MAVEN_ARGS} versions:set -DnewVersion="${RELEASE_VERSION:1}" versions:commit env: RELEASE_VERSION: ${{ github.event.release.tag_name }} -# - name: Release Maven package -# uses: samuelmeuli/action-maven-publish@v1 -# with: -# maven_profiles: "release" -# gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} -# gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} -# nexus_username: ${{ secrets.OSSRH_USERNAME }} -# nexus_password: ${{ secrets.OSSRH_TOKEN }} + - name: Release Maven package + uses: samuelmeuli/action-maven-publish@v1 + with: + maven_profiles: "release" + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} + nexus_username: ${{ secrets.OSSRH_USERNAME }} + nexus_password: ${{ secrets.OSSRH_TOKEN }} # This is separate job because there were issues with git after release step, was not able to commit changes. See history. update-working-version: diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index fc055ea220..2866f42257 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 41ecb0f137..af0737bf29 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 0339129c75..7a9977ded0 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 0c05632099..2fe67f8716 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 809c25752c..1517bd034c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 878c4a81df..5d9a497cec 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 49144bf646..93ca257ed5 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 7a6c711df5..34286abadc 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 0038730ea3..ed1884aecd 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 2d1ad2923a..566fbd98be 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 9fb7f1c92c..d1a2de4c2b 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index cbdcabe77a..de64a684e5 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 5a4ddce22fed450863e8dc516bff163a4a16237d Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 25 Nov 2021 10:18:59 +0100 Subject: [PATCH 0153/1608] feature: use main branch istead of master in worflows --- .github/workflows/master-snapshot-release.yml | 2 +- .github/workflows/pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index 1fb5ac7e2e..69e3e009cb 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: true on: push: - branches: [ master ] + branches: [ main ] workflow_dispatch: jobs: test: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 63ea4ee489..5a9287ce08 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: true on: pull_request: - branches: [ master, v1 ] + branches: [ main, v1 ] workflow_dispatch: jobs: build: From b91221bb54af19761a617bf18eef381e8ceb3b4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Nov 2021 10:41:10 +0100 Subject: [PATCH 0154/1608] chore(deps-dev): bump junit from 4.13.1 to 4.13.2 (#711) Bumps [junit](https://github.com/junit-team/junit4) from 4.13.1 to 4.13.2. - [Release notes](https://github.com/junit-team/junit4/releases) - [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.13.1.md) - [Commits](https://github.com/junit-team/junit4/compare/r4.13.1...r4.13.2) --- updated-dependencies: - dependency-name: junit:junit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sample-operators/tomcat-operator/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 93ca257ed5..ae8605f305 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -45,7 +45,7 @@ junit junit - 4.13.1 + 4.13.2 test From 2e01de0bb732f5ca9243f680b761edd1f699c5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 25 Nov 2021 15:49:07 +0100 Subject: [PATCH 0155/1608] Docs updates for v2 (#715) --- README.md | 234 ++++++++---------- docs/documentation/features.md | 133 ++++++---- docs/documentation/getting-started.md | 2 +- docs/documentation/use-samples.md | 176 +++++++------ .../api/reconciler/ErrorStatusHandler.java | 8 +- .../reconciler/EventSourceInitializer.java | 4 +- .../operator/api/reconciler/RetryInfo.java | 8 +- .../api/reconciler/UpdateControl.java | 7 +- 8 files changed, 296 insertions(+), 276 deletions(-) diff --git a/README.md b/README.md index 790169d036..b211f563d8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Build Kubernetes Operators in Java without hassle. Inspired by [operator-sdk](https://github.com/operator-framework/operator-sdk). -Our webpage with documentation is getting better every day: https://javaoperatorsdk.io/ +Our webpage with documentation is getting better every day: https://javaoperatorsdk.io/ Table of Contents ========== @@ -23,11 +23,13 @@ Table of Contents * Automatic registration of Custom Resource watches * Retry action on failure * Smart event scheduling (only handle the latest event for the same resource) +* Handling Observed Generations automatically +* for all see [features](https://javaoperatorsdk.io/docs/features) section on the webpage Check out this [blog post](https://csviri.medium.com/deep-dive-building-a-kubernetes-operator-sdk-for-java-developers-5008218822cb) -about the non-trivial yet common problems needed to be solved for every operator. In case you are -interested how to handle more complex scenarios take a look +about the non-trivial yet common problems needed to be solved for every operator. In case you are interested how to +handle more complex scenarios take a look on [event sources](https://csviri.medium.com/java-operator-sdk-introduction-to-event-sources-a1aab5af4b7b) . @@ -40,22 +42,16 @@ on [event sources](https://csviri.medium.com/java-operator-sdk-introduction-to-e * Integration with Cloud services - e.g. Secret stores * Safer deployment of applications - only expose cluster to users by Custom Resources -## Roadmap and Release Notes - -* Testing of the framework and all samples while running on a real cluster. -* Generate a project skeleton -* Generate Java classes from CRD -* Integrate with OLM (Operator Lifecycle Manager) - #### Overview of the 1.9.0 changes - The Spring Boot starters have been moved to their own repositories and are now found at: - https://github.com/java-operator-sdk/operator-framework-spring-boot-starter - https://github.com/java-operator-sdk/operator-framework-spring-boot-starter-test - Updated Fabric8 client to version 5.4.0 -- It is now possible to configure the controllers to not automatically add finalizers to resources. - See the `Controller` annotation documentation for more details. -- Added the possibility to configure how many seconds the SDK will wait before terminating reconciliation threads when a shut down is requested +- It is now possible to configure the controllers to not automatically add finalizers to resources. See the `Controller` + annotation documentation for more details. +- Added the possibility to configure how many seconds the SDK will wait before terminating reconciliation threads when a + shut down is requested #### Overview of the 1.8.0 changes @@ -65,28 +61,26 @@ on [event sources](https://csviri.medium.com/java-operator-sdk-introduction-to-e ##### Overview of the 1.7.0 changes - `Doneable` classes have been removed along with all the involved complexity -- `Controller` annotation has been simplified: the `crdName` field has been removed as that value is - computed from the associated custom resource implementation +- `Controller` annotation has been simplified: the `crdName` field has been removed as that value is computed from the + associated custom resource implementation - Custom Resource implementation classes now need to be annotated with `Group` and `Version` annotations so that they can be identified properly. Optionally, they can also be annotated with - `Kind` (if the name of the implementation class doesn't match the desired kind) and `Plural` if - the plural version cannot be automatically computed (or the default computed version doesn't match - your expectations). -- The `CustomResource` class that needs to be extended is now parameterized with spec and status - types, so you can have an empty default implementation that does what you'd expect. If you don't - need a status, using `Void` for the associated type should work. -- Custom Resources that are namespace-scoped need to implement the `Namespaced` interface so that - the client can generate the proper URLs. This means, in particular, that `CustomResource` - implementations that do **not** implement `Namespaced` are considered cluster-scoped. As a - consequence, the `isClusterScoped` method/field has been removed from the appropriate - classes (`Controller` annotation, in particular) as this is now inferred from the `CustomResource` + `Kind` (if the name of the implementation class doesn't match the desired kind) and `Plural` if the plural version + cannot be automatically computed (or the default computed version doesn't match your expectations). +- The `CustomResource` class that needs to be extended is now parameterized with spec and status types, so you can have + an empty default implementation that does what you'd expect. If you don't need a status, using `Void` for the + associated type should work. +- Custom Resources that are namespace-scoped need to implement the `Namespaced` interface so that the client can + generate the proper URLs. This means, in particular, that `CustomResource` + implementations that do **not** implement `Namespaced` are considered cluster-scoped. As a consequence, + the `isClusterScoped` method/field has been removed from the appropriate classes (`Controller` annotation, in + particular) as this is now inferred from the `CustomResource` type associated with your `Controller`. -Many of these changes might not be immediately apparent but will result in `404` errors when -connecting to the cluster. Please check that the Custom Resource implementations are properly -annotated and that the value corresponds to your CRD manifest. If the namespace appear to be missing -in your request URL, don't forget that namespace-scoped Custom Resources need to implement -the `Namescaped` interface. +Many of these changes might not be immediately apparent but will result in `404` errors when connecting to the cluster. +Please check that the Custom Resource implementations are properly annotated and that the value corresponds to your CRD +manifest. If the namespace appear to be missing in your request URL, don't forget that namespace-scoped Custom Resources +need to implement the `Namescaped` interface. ## Join us on Discord! @@ -94,17 +88,15 @@ the `Namescaped` interface. ## Usage -We have several sample Operators under the [samples](samples) directory: +We have several simple Operators under the [smoke-test-samples](smoke-test-samples) directory: -* *pure-java*: Minimal Operator implementation which only parses the Custom Resource and prints to - stdout. Implemented with and without Spring Boot support. The two samples share the common module. +* *pure-java*: Minimal Operator implementation which only parses the Custom Resource and prints to stdout. Implemented + with and without Spring Boot support. The two samples share the common module. * *spring-boot-plain*: Sample showing integration with Spring Boot. -There are also more samples in the -standalone [samples repo](https://github.com/java-operator-sdk/samples): +There are also more complete samples in the standalone [sample-operators](sample-operators): -* *webserver*: Simple example creating an NGINX webserver from a Custom Resource containing HTML - code. +* *webserver*: Simple example creating an NGINX webserver from a Custom Resource containing HTML code. * *mysql-schema*: Operator managing schemas in a MySQL database. * *tomcat*: Operator with two controllers, managing Tomcat instances and Webapps for these. @@ -113,14 +105,14 @@ Add [dependency](https://search.maven.org/search?q=a:operator-framework) to your ```xml - io.javaoperatorsdk - operator-framework - {see https://search.maven.org/search?q=a:operator-framework for latest version} + io.javaoperatorsdk + operator-framework + {see https://search.maven.org/search?q=a:operator-framework for latest version} ``` -Or alternatively with Gradle, which also requires declaring the SDK as an annotation processor to -generate the mappings between controllers and custom resource classes: +Or alternatively with Gradle, which also requires declaring the SDK as an annotation processor to generate the mappings +between controllers and custom resource classes: ```groovy dependencies { @@ -129,16 +121,16 @@ dependencies { } ``` -Once you've added the dependency, define a main method initializing the Operator and registering a -controller. +Once you've added the dependency, define a main method initializing the Operator and registering a controller. ```java public class Runner { - public static void main(String[] args) { - Operator operator = new Operator(DefaultConfigurationService.instance()); - operator.register(new WebServerController()); - } + public static void main(String[] args) { + Operator operator = new Operator(DefaultConfigurationService.instance()); + operator.register(new WebPageReconciler()); + operator.start(); + } } ``` @@ -147,15 +139,15 @@ The Controller implements the business logic and describes all the classes neede ```java @ControllerConfiguration -public class WebServerController implements ResourceController { - - // Return the changed resource, so it gets updated. See javadoc for details. - @Override - public UpdateControl createOrUpdateResource(CustomService resource, - Context context) { - // ... your logic ... - return UpdateControl.updateStatusSubResource(resource); - } +public class WebServerController implements Reconciler { + + // Return the changed resource, so it gets updated. See javadoc for details. + @Override + public UpdateControl reconcile(CustomService resource, + Context context) { + // ... your logic ... + return UpdateControl.updateStatus(resource); + } } ``` @@ -165,52 +157,50 @@ A sample custom resource POJO representation @Group("sample.javaoperatorsdk") @Version("v1") -public class WebServer extends CustomResource implements - Namespaced { +public class WebPage extends CustomResource implements + Namespaced { } -public class WebServerSpec { +public class WebPageSpec { - private String html; + private String html; - public String getHtml() { - return html; - } + public String getHtml() { + return html; + } - public void setHtml(String html) { - this.html = html; - } + public void setHtml(String html) { + this.html = html; + } } ``` ### Deactivating CustomResource implementations validation The operator will, by default, query the deployed CRDs to check that the `CustomResource` -implementations match what is known to the cluster. This requires an additional query to the cluster -and, sometimes, elevated privileges for the operator to be able to read the CRDs from the cluster. -This validation is mostly meant to help users new to operator development get started and avoid -common mistakes. Advanced users or production deployments might want to skip this step. This is done -by setting the `CHECK_CRD_ENV_KEY` environment variable to `false`. +implementations match what is known to the cluster. This requires an additional query to the cluster and, sometimes, +elevated privileges for the operator to be able to read the CRDs from the cluster. This validation is mostly meant to +help users new to operator development get started and avoid common mistakes. Advanced users or production deployments +might want to skip this step. This is done by setting the `CHECK_CRD_ENV_KEY` environment variable to `false`. ### Automatic generation of CRDs -To automatically generate CRD manifests from your annotated Custom Resource classes, you only need -to add the following dependencies to your project: +To automatically generate CRD manifests from your annotated Custom Resource classes, you only need to add the following +dependencies to your project: ```xml - io.fabric8 - crd-generator-apt - provided + io.fabric8 + crd-generator-apt + provided ``` -The CRD will be generated in `target/classes/META-INF/fabric8` (or -in `target/test-classes/META-INF/fabric8`, if you use the `test` scope) with the CRD name suffixed -by the generated spec version. For example, a CR using the `java-operator-sdk.io` group with -a `mycrs` plural form will result in 2 files: +The CRD will be generated in `target/classes/META-INF/fabric8` (or in `target/test-classes/META-INF/fabric8`, if you use +the `test` scope) with the CRD name suffixed by the generated spec version. For example, a CR using +the `java-operator-sdk.io` group with a `mycrs` plural form will result in 2 files: - `mycrs.java-operator-sdk.io-v1.yml` - `mycrs.java-operator-sdk.io-v1beta1.yml` @@ -220,8 +210,7 @@ a `mycrs` plural form will result in 2 files: ### Quarkus -A [Quarkus](https://quarkus.io) extension is also provided to ease the development of Quarkus-based -operators. +A [Quarkus](https://quarkus.io) extension is also provided to ease the development of Quarkus-based operators. Add [this dependency](https://search.maven.org/search?q=a:quarkus-operator-sdk) to your project: @@ -229,55 +218,52 @@ to your project: ```xml - io.quarkiverse.operatorsdk - quarkus-operator-sdk - {see https://search.maven.org/search?q=a:quarkus-operator-sdk for latest version} - + io.quarkiverse.operatorsdk + quarkus-operator-sdk + {see https://search.maven.org/search?q=a:quarkus-operator-sdk for latest version} + ``` Create an Application, Quarkus will automatically create and inject a `KubernetesClient` ( -or `OpenShiftClient`), `Operator`, `ConfigurationService` and `ResourceController` instances that -your application can use. Below, you can see the minimal code you need to write to get your operator -and controllers up and running: +or `OpenShiftClient`), `Operator`, `ConfigurationService` and `ResourceController` instances that your application can +use. Below, you can see the minimal code you need to write to get your operator and controllers up and running: ```java @QuarkusMain public class QuarkusOperator implements QuarkusApplication { - @Inject - Operator operator; + @Inject + Operator operator; - public static void main(String... args) { - Quarkus.run(QuarkusOperator.class, args); - } + public static void main(String... args) { + Quarkus.run(QuarkusOperator.class, args); + } - @Override - public int run(String... args) throws Exception { - operator.start(); - Quarkus.waitForExit(); - return 0; - } + @Override + public int run(String... args) throws Exception { + operator.start(); + Quarkus.waitForExit(); + return 0; + } } ``` ### Spring Boot -You can also let Spring Boot wire your application together and automatically register the -controllers. +You can also let Spring Boot wire your application together and automatically register the controllers. -Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter) to -your project: +Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter) to your project: ```xml - io.javaoperatorsdk - operator-framework-spring-boot-starter - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for - latest version} - + io.javaoperatorsdk + operator-framework-spring-boot-starter + {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for + latest version} + ``` @@ -288,25 +274,25 @@ Create an Application @SpringBootApplication public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } } ``` #### Spring Boot test support -Adding the following dependency would let you mock the operator for the tests where loading the -spring container is necessary, but it doesn't need real access to a Kubernetes cluster. +Adding the following dependency would let you mock the operator for the tests where loading the spring container is +necessary, but it doesn't need real access to a Kubernetes cluster. ```xml - io.javaoperatorsdk - operator-framework-spring-boot-starter-test - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for - latest version} - + io.javaoperatorsdk + operator-framework-spring-boot-starter-test + {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for + latest version} + ``` @@ -318,8 +304,8 @@ Mock the operator: @EnableMockOperator public class SpringBootStarterSampleApplicationTest { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } ``` diff --git a/docs/documentation/features.md b/docs/documentation/features.md index d281fd1a22..194effb36e 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -11,21 +11,21 @@ Java Operator SDK is a high level framework and related tooling in order to faci operators. The features are by default following the best practices in an opinionated way. However, feature flags and other configuration options are provided to fine tune or turn off these features. -## Controller Execution in a Nutshell +## Reconciliation Execution in a Nutshell -Controller execution is always triggered by an event. Events typically come from the custom resource +Reconciliation execution is always triggered by an event. Events typically come from the custom resource (i.e. custom resource is created, updated or deleted) that the controller is watching, but also from different sources (see event sources). When an event is received reconciliation is executed, unless there is already a reconciliation happening for a particular custom resource. In other words it is guaranteed by the framework that no concurrent reconciliation happens for a custom resource. After a reconciliation ( -i.e. [ResourceController](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java) +i.e. [Reconciler](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) called, a post-processing phase follows, where typically framework checks if: - an exception was thrown during execution, if yes schedules a retry. - there are new events received during the controller execution, if yes schedule the execution again. -- there is an instruction to re-schedule the execution for the future, if yes schedule a timer event with the specified +- there is an instruction to re-schedule the execution for the future, if yes schedules a timer event with the specified delay. - if none above, the reconciliation is finished. @@ -36,108 +36,138 @@ execution. [Kubernetes finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) make sure that a reconciliation happens when a custom resource is instructed to be deleted. Typical case when it's -useful, when an operator is down (pod not running). Without a finalizer the reconciliation - thus the cleanup -i.e. [`Reconciler.cleanup(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java) +useful, when an operator is down (pod not running). Without a finalizer the reconciliation - thus the cleanup - +i.e. [`Reconciler.cleanup(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java#L31) +would not happen if a custom resource is deleted. -- would not happen if a custom resource is deleted. - -Finalizers are automatically added by the framework as the first step, thus when a custom resource is created, but -before the first reconciliation, the custom resource is updated via a Kubernetes API call. As a result of this update, +Finalizers are automatically added by the framework as the first step, thus after a custom resource is created, but +before the first reconciliation. The finalizer is added via a separate Kubernetes API call. As a result of this update, the finalizer will be present. The subsequent event will be received, which will trigger the first reconciliation. -The finalizer that is automatically added will be also removed after the `deleteResource` is executed on the -controllerConfiguration. However, the removal behavior can be further customized, and can be instructed to "not remove -yet" - this is useful just in some specific corner cases, when there would be a long waiting period for some dependent -resource cleanup. +The finalizer that is automatically added will be also removed after the `cleanup` is executed on the reconciler. +However, the removal behavior can be further customized, and can be instructed to "not remove yet" - this is useful just +in some specific corner cases, when there would be a long waiting period for some dependent resource cleanup. The name of the finalizers can be specified, in case it is not, a name will be generated. -This behavior can be turned off, so when configured no finalizer will be added or removed. +Automatic finalizer handling can be turned off, so when configured no finalizer will be added or removed. See [`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ControllerConfiguration.java) annotation for more details. ### When not to Use Finalizers? -Typically, automated finalizer handling should be turned off, in case **all** the cleanup of the dependent resources is +Typically, automated finalizer handling should be turned off, in case the cleanup of **all** the dependent resources is handled by Kubernetes itself. This is handled by Kubernetes [garbage collection](https://kubernetes.io/docs/concepts/architecture/garbage-collection/#owners-dependents). -Setting the owner reference and related fields are not in the scope of the SDK for now, it's up to the user to have them -configured properly when creating the objects. +Setting the owner reference and related fields are not in the scope of the SDK, it's up to the user to have them set +properly when creating the objects. -When automatic finalizer handling is turned off, the `ResourceController.deleteResource(...)` method is not called, in -case of a delete event received. So it does not make sense to implement this method and turn off finalizer at the same +When automatic finalizer handling is turned off, the `Reconciler.cleanup(...)` method is not called at all. Not even in +case when a delete event received. So it does not make sense to implement this method and turn off finalizer at the same time. ## The `reconcile` and `cleanup` Methods of `Reconciler` The lifecycle of a custom resource can be clearly separated to two phases from a perspective of an operator. When a -custom resource is created or update, or on the other hand when the custom resource is deleted - or rater marked for +custom resource is created or update, or on the other hand when the custom resource is deleted - or rather marked for deletion in case a finalizer is used. -There is no point to make a distinction between create and update, since the reconciliation logic typically would be -very similar or identical in most of the cases. - -This separation related logic is automatically handled by framework. The framework will always -call `createOrUpdateResource` -function, unless the custom resource is +This separation related logic is automatically handled by framework. The framework will always call `reconcile` +method, unless the custom resource is [marked from deletion](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/#how-finalizers-work) -. From the point when the custom resource is marked from deletion, only the `deleteResource` method is called. +. From the point when the custom resource is marked from deletion, only the `cleanup` method is called. -If there is **no finalizer** in place (see Finalizer Support section), the `deleteResource` method is **not called**. +If there is **no finalizer** in place (see Finalizer Support section), the `cleanup` method is **not called**. ### Using `UpdateControl` and `DeleteControl` -These two methods are used to control the outcome or the desired behavior after the reconciliation. +These two classes are used to control the outcome or the desired behavior after the reconciliation. -The `UpdateControl` can instruct the framework to update the custom resource status sub-resource and/or re-schedule a +The `UpdateControl` can instruct the framework to update the status sub-resource of the resource and/or re-schedule a reconciliation with a desired time delay. Those are the typical use cases, however in some cases there it can happen that the controller wants to update the custom resource itself (like adding annotations) or not to do any updates, which -are also supported. +is also supported. It is also possible to update both the status and the custom resource with `updateCustomResourceAndStatus` method. In this case first the custom resource is updated then the status in two separate requests to K8S API. Always update the custom resource with `UpdateControl`, not with the actual kubernetes client if possible. -On custom resource updates there is always an optimistic version control in place, to make sure that another update is -not overwritten (by setting `resourceVersion` ) . +On resource updates there is always an optimistic version control in place, to make sure that another update is not +overwritten (by setting `resourceVersion` ) . The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent resource are cleaned -up in `deleteResource` implementation. +up in `cleanup` implementation. However, there is a possibility to not remove the finalizer, this allows to clean up the resources in a more async way, mostly for the cases when there is a long waiting period after a delete operation is initiated. Note that in this case you might want to either schedule a timed event to make sure the `deleteResource` is executed again or use event sources get notified about the state changes of a deleted resource. -## Automatic Observed Generation Handling +## Generation Awareness and Automatic Observed Generation Handling + +Having `.observedGeneration` value on the status of the resource is a best practice to indicate the last generation of +the resource reconciled successfully by the controller. This helps the users / administrators to check if the custom +resource was reconciled, but it is used to decide if a reconciliation should happen or not. Filtering events based on +generation is supported by the framework and turned on by default. There are two modes. + +The first and the **preferred** one is to check after a resource event received, if the generation of the resource is +larger than the `.observedGeneration` field on status. In order to have this feature working: + +- the **status class** (not the resource) must implement the + [`ObservedGenerationAware`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java) + interface. See also + the [`ObservedGenerationAwareStatus`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java) + which can be also extended. +- The other condition is that the `CustomResource.getStatus()` method should not return `null` + , but an instance of the class representing `status`. The best way to achieve this is to + override [`CustomResource.initStatus()`](https://github.com/fabric8io/kubernetes-client/blob/865e0ddf67b99f954aa55ab14e5806d53ae149ec/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/CustomResource.java#L139). + +If these conditions are fulfilled and generation awareness not turned off, the observed generation is automatically set +by the framework after the `reconcile` method is called. There is just one exception, when the reconciler returns +with `UpdateControl.updateResource`, in this case the status is not updated, just the custom resource - however, this +update will lead to a new reconciliation. Note that the observed generation is updated also +when `UpdateControl.noUpdate()` is returned from the reconciler. See this feature working in +the [WebPage example](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java#L5) +. + +The second, fallback mode is (when the conditions from above are not met to handle the observed generation automatically +in status) to handled generation filtering in memory. Thus, if an event is received, the generation of the received +resource is compared with the resource in the cache. + +Note that the **first approach has significant benefits** in the situation when the operator is restarted and there is +no cached resource anymore. In case two this leads to a reconciliation of every resource, event if the resource is not +changed while the operator was not running. ## Support for Well Known (non-custom) Kubernetes Resources A Controller can be registered for a non-custom resource, so well known Kubernetes resources like ( -Ingress,Deployment,...). Note that automatic observed generation handling is not supported for these resources. +Ingress,Deployment,...). Note that automatic observed generation handling is not supported for these resources. Although +in case adding a secondary controller for well known k8s resource, probably the observed generation should be handled by +the primary controller. ## Automatic Retries on Error When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. The retry is behavior is configurable, an implementation is provided that should cover most of the use-cases, see [GenericRetry](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java) -But it is possible to provide a custom implementation. +. But it is possible to provide a custom implementation. It is possible to set a limit on the number of retries. In the [Context](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java) -object information is provided about the retry, particularly interesting is the `isLastAttempt`, since a behavior could -be implemented bases on this flag. Like setting an error message in the status in case of a last attempt; +object information is provided about the retry, particularly interesting is the `isLastAttempt`, since a different +behavior could be implemented bases on this flag. Like setting an error message in the status in case of a last attempt; Event if the retry reached a limit, in case of a new event is received the reconciliation would happen again, it's just -won't be a result of a retry, but the new event. +won't be a result of a retry, but the new event. However, in case of an error happens also in this case, it won't +schedule a retry is at this point the retry limit is already reached. A successful execution resets the retry. ### Setting Error Status After Last Retry Attempt In order to facilitate error reporting in case a last retry attempt fails, Reconciler can implement the following -interface: +[interface](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java): ```java public interface ErrorStatusHandler> { @@ -150,13 +180,12 @@ public interface ErrorStatusHandler> { The `updateErrorStatus` resource is called when it's the last retry attempt according the retry configuration and the reconciler execution still resulted in a runtime exception. -The result of the method call is used to make a status sub-resource update on the custom resource. This is always a -sub-resource update request, so no update on custom resource itself (like spec of metadata) happens. Note that this -update request will also produce an event, and will result in a reconciliation if the controller is not generation -aware. +The result of the method call is used to make a status update on the custom resource. This is always a sub-resource +update request, so no update on custom resource itself (like spec of metadata) happens. Note that this update request +will also produce an event, and will result in a reconciliation if the controller is not generation aware. -Note that the scope of this feature is only the `reconcile` method of the reconciler, since there should not be updates -on custom resource after it is marked for deletion. +The scope of this feature is only the `reconcile` method of the reconciler, since there should not be updates on custom +resource after it is marked for deletion. ### Correctness and Automatic Retries @@ -175,7 +204,7 @@ explicitly by `UpdateControl`, see method: `public UpdateControl rescheduleAfter(long delay, TimeUnit timeUnit)`. This would schedule a reconciliation for the future. -## Retry and Rescheduling and Unrelated Event Handling Common Behavior +## Retry and Rescheduling and Event Handling Common Behavior Retry, reschedule and standard event processing forms a relatively complex system, where these functionalities are not independent of each other. In the following we describe the behavior in this section, so it is easier to understand the @@ -183,11 +212,11 @@ intersections: 1. A successful execution resets a retry and the rescheduled executions which were present before the reconciliation. However, a new rescheduling can be instructed from the reconciliation outcome (`UpdateControl` or `DeleteControl`). -2. In case an exception happened, and a retry is initiated, but an event received meanwhile the reconciliation will be +2. In case an exception happened, and a retry is initiated, but an event received meanwhile, then reconciliation will be executed instantly, and this execution won't count as a retry attempt. 3. If the retry limit is reached (so no more automatic retry would happen), but a new event received, the reconciliation will still happen, but won't reset the retry, will be still marked as the last attempt in the retry info. The point - 1. holds, but in case of an error, no retry will happen. + (1) still holds, but in case of an error, no retry will happen. ## Handling Related Events with Event Sources @@ -220,6 +249,8 @@ we could read the object again from Kubernetes API. However since we watch for t receive the most up-to-date version in the Event Source. So naturally, what we can do is cache the latest received objects (in the Event Source) and read it from there if needed. +### Implementing and EventSource + ### Built-in Event Sources 1. InformerEventSource - used to get event about other K8S resources, also provides a local cache for them. diff --git a/docs/documentation/getting-started.md b/docs/documentation/getting-started.md index 3ca64df871..04f9b68b7d 100644 --- a/docs/documentation/getting-started.md +++ b/docs/documentation/getting-started.md @@ -10,7 +10,7 @@ permalink: /docs/getting-started ## Introduction & Resources on Operators Operators are easy and simple way to manage resource on Kubernetes clusters but -also outside of the cluster. The goal of this SDK is to allow writing operators in Java by +also outside the cluster. The goal of this SDK is to allow writing operators in Java by providing a nice API and handling common issues regarding the operators on framework level. For an introduction, what is an operator see this [blog post](https://blog.container-solutions.com/kubernetes-operators-explained). diff --git a/docs/documentation/use-samples.md b/docs/documentation/use-samples.md index a736aabe49..c4d6f99533 100644 --- a/docs/documentation/use-samples.md +++ b/docs/documentation/use-samples.md @@ -5,20 +5,18 @@ layout: docs permalink: /docs/using-samples --- - # How to use sample Operators -We have several sample Operators under the [samples](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/samples) directory: +We have several sample Operators under +the [samples](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/smoke-test-samples) directory: -* *pure-java*: Minimal Operator implementation which only parses the Custom Resource and prints to - stdout. Implemented with and without Spring Boot support. The two samples share the common module. +* *pure-java*: Minimal Operator implementation which only parses the Custom Resource and prints to stdout. Implemented + with and without Spring Boot support. The two samples share the common module. * *spring-boot-plain*: Sample showing integration with Spring Boot. -There are also more samples in the -standalone [samples repo](https://github.com/java-operator-sdk/samples): +There are also more samples in the standalone [samples repo](https://github.com/java-operator-sdk/samples): -* *webserver*: Simple example creating an NGINX webserver from a Custom Resource containing HTML - code. +* *webserver*: Simple example creating an NGINX webserver from a Custom Resource containing HTML code. * *mysql-schema*: Operator managing schemas in a MySQL database. * *tomcat*: Operator with two controllers, managing Tomcat instances and Webapps for these. @@ -27,14 +25,14 @@ Add [dependency](https://search.maven.org/search?q=a:operator-framework) to your ```xml - io.javaoperatorsdk - operator-framework - {see https://search.maven.org/search?q=a:operator-framework for latest version} + io.javaoperatorsdk + operator-framework + {see https://search.maven.org/search?q=a:operator-framework for latest version} ``` -Or alternatively with Gradle, which also requires declaring the SDK as an annotation processor to -generate the mappings between controllers and custom resource classes: +Or alternatively with Gradle, which also requires declaring the SDK as an annotation processor to generate the mappings +between controllers and custom resource classes: ```groovy dependencies { @@ -43,16 +41,16 @@ dependencies { } ``` -Once you've added the dependency, define a main method initializing the Operator and registering a -controller. +Once you've added the dependency, define a main method initializing the Operator and registering a controller. ```java public class Runner { - public static void main(String[] args) { - Operator operator = new Operator(DefaultConfigurationService.instance()); - operator.register(new WebServerController()); - } + public static void main(String[] args) { + Operator operator = new Operator(DefaultConfigurationService.instance()); + operator.register(new WebServerController()); + operator.start(); + } } ``` @@ -61,15 +59,15 @@ The Controller implements the business logic and describes all the classes neede ```java @Controller -public class WebServerController implements ResourceController { - - // Return the changed resource, so it gets updated. See javadoc for details. - @Override - public UpdateControl createOrUpdateResource(CustomService resource, - Context context) { - // ... your logic ... - return UpdateControl.updateStatusSubResource(resource); - } +public class WebServerController implements Reconciler { + + // Return the changed resource, so it gets updated. See javadoc for details. + @Override + public UpdateControl reconcile(CustomService resource, + Context context) { + // ... your logic ... + return UpdateControl.updateStatus(resource); + } } ``` @@ -80,51 +78,49 @@ A sample custom resource POJO representation @Group("sample.javaoperatorsdk") @Version("v1") public class WebServer extends CustomResource implements - Namespaced { + Namespaced { } public class WebServerSpec { - private String html; + private String html; - public String getHtml() { - return html; - } + public String getHtml() { + return html; + } - public void setHtml(String html) { - this.html = html; - } + public void setHtml(String html) { + this.html = html; + } } ``` ### Deactivating CustomResource implementations validation The operator will, by default, query the deployed CRDs to check that the `CustomResource` -implementations match what is known to the cluster. This requires an additional query to the cluster -and, sometimes, elevated privileges for the operator to be able to read the CRDs from the cluster. -This validation is mostly meant to help users new to operator development get started and avoid -common mistakes. Advanced users or production deployments might want to skip this step. This is done -by setting the `CHECK_CRD_ENV_KEY` environment variable to `false`. +implementations match what is known to the cluster. This requires an additional query to the cluster and, sometimes, +elevated privileges for the operator to be able to read the CRDs from the cluster. This validation is mostly meant to +help users new to operator development get started and avoid common mistakes. Advanced users or production deployments +might want to skip this step. This is done by setting the `CHECK_CRD_ENV_KEY` environment variable to `false`. ### Automatic generation of CRDs -To automatically generate CRD manifests from your annotated Custom Resource classes, you only need -to add the following dependencies to your project: +To automatically generate CRD manifests from your annotated Custom Resource classes, you only need to add the following +dependencies to your project: ```xml - io.fabric8 - crd-generator-apt - provided + io.fabric8 + crd-generator-apt + provided ``` -The CRD will be generated in `target/classes/META-INF/fabric8` (or -in `target/test-classes/META-INF/fabric8`, if you use the `test` scope) with the CRD name suffixed -by the generated spec version. For example, a CR using the `java-operator-sdk.io` group with -a `mycrs` plural form will result in 2 files: +The CRD will be generated in `target/classes/META-INF/fabric8` (or in `target/test-classes/META-INF/fabric8`, if you use +the `test` scope) with the CRD name suffixed by the generated spec version. For example, a CR using +the `java-operator-sdk.io` group with a `mycrs` plural form will result in 2 files: - `mycrs.java-operator-sdk.io-v1.yml` - `mycrs.java-operator-sdk.io-v1beta1.yml` @@ -134,8 +130,7 @@ a `mycrs` plural form will result in 2 files: ### Quarkus -A [Quarkus](https://quarkus.io) extension is also provided to ease the development of Quarkus-based -operators. +A [Quarkus](https://quarkus.io) extension is also provided to ease the development of Quarkus-based operators. Add [this dependency](https://search.maven.org/search?q=a:quarkus-operator-sdk) to your project: @@ -143,55 +138,52 @@ to your project: ```xml - io.quarkiverse.operatorsdk - quarkus-operator-sdk - {see https://search.maven.org/search?q=a:quarkus-operator-sdk for latest version} - + io.quarkiverse.operatorsdk + quarkus-operator-sdk + {see https://search.maven.org/search?q=a:quarkus-operator-sdk for latest version} + ``` Create an Application, Quarkus will automatically create and inject a `KubernetesClient` ( -or `OpenShiftClient`), `Operator`, `ConfigurationService` and `ResourceController` instances that -your application can use. Below, you can see the minimal code you need to write to get your operator -and controllers up and running: +or `OpenShiftClient`), `Operator`, `ConfigurationService` and `ResourceController` instances that your application can +use. Below, you can see the minimal code you need to write to get your operator and controllers up and running: ```java @QuarkusMain public class QuarkusOperator implements QuarkusApplication { - @Inject - Operator operator; + @Inject + Operator operator; - public static void main(String... args) { - Quarkus.run(QuarkusOperator.class, args); - } + public static void main(String... args) { + Quarkus.run(QuarkusOperator.class, args); + } - @Override - public int run(String... args) throws Exception { - operator.start(); - Quarkus.waitForExit(); - return 0; - } + @Override + public int run(String... args) throws Exception { + operator.start(); + Quarkus.waitForExit(); + return 0; + } } ``` ### Spring Boot -You can also let Spring Boot wire your application together and automatically register the -controllers. +You can also let Spring Boot wire your application together and automatically register the controllers. -Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter) to -your project: +Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter) to your project: ```xml - io.javaoperatorsdk - operator-framework-spring-boot-starter - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for - latest version} - + io.javaoperatorsdk + operator-framework-spring-boot-starter + {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for + latest version} + ``` @@ -202,25 +194,25 @@ Create an Application @SpringBootApplication public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } } ``` #### Spring Boot test support -Adding the following dependency would let you mock the operator for the tests where loading the -spring container is necessary, but it doesn't need real access to a Kubernetes cluster. +Adding the following dependency would let you mock the operator for the tests where loading the spring container is +necessary, but it doesn't need real access to a Kubernetes cluster. ```xml - io.javaoperatorsdk - operator-framework-spring-boot-starter-test - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for - latest version} - + io.javaoperatorsdk + operator-framework-spring-boot-starter-test + {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for + latest version} + ``` @@ -232,8 +224,8 @@ Mock the operator: @EnableMockOperator public class SpringBootStarterSampleApplicationTest { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } } ``` diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java index 578d3ff8a6..2d5592053b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java @@ -10,10 +10,10 @@ public interface ErrorStatusHandler { * when the last reconciliation retry attempt is failed on the Reconciler. In that case the * updateErrorStatus is called automatically. *

- * The result of the method call is used to make a status sub-resource update on the custom - * resource. This is always a sub-resource update request, so no update on custom resource itself - * (like spec of metadata) happens. Note that this update request will also produce an event, and - * will result in a reconciliation if the controller is not generation aware. + * The result of the method call is used to make a status update on the custom resource. This is + * always a sub-resource update request, so no update on custom resource itself (like spec of + * metadata) happens. Note that this update request will also produce an event, and will result in + * a reconciliation if the controller is not generation aware. *

* Note that the scope of this feature is only the reconcile method of the reconciler, since there * should not be updates on custom resource after it is marked for deletion. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index b568814739..5a23e0da16 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -6,8 +6,8 @@ public interface EventSourceInitializer { /** - * In this typically you might want to register event sources. But can access - * CustomResourceEventSource, what might be handy for some edge cases. + * Reconciler can implement this interface typically to register event sources. But can access + * CustomResourceEventSource, what might be useful for some edge cases. * * @param eventSourceRegistry the {@link EventSourceRegistry} where event sources can be * registered. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RetryInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RetryInfo.java index 2525bde192..f746c20dce 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RetryInfo.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/RetryInfo.java @@ -1,8 +1,14 @@ package io.javaoperatorsdk.operator.api.reconciler; public interface RetryInfo { - + /** + * @return current retry attempt count. 0 if the current execution is not a retry. + */ int getAttemptCount(); + /** + * @return true, if the current attempt is the last one in regard to the retry limit + * configuration. + */ boolean isLastAttempt(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index 1affc25abd..c388958a8a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -19,6 +19,11 @@ private UpdateControl( this.updateResource = updateResource; } + /** + * Creates an update control instance that instructs the framework to do an update on resource + * itself, not on the status. Note that usually as a results of a reconciliation should be a + * status update not an update to the resource itself. + */ public static UpdateControl updateResource(T customResource) { return new UpdateControl<>(customResource, false, true); } @@ -31,7 +36,7 @@ public static UpdateControl updateStatus( /** * As a results of this there will be two call to K8S API. First the custom resource will be * updates then the status sub-resource. - * + * * @param resource type * @param customResource - custom resource to use in both API calls * @return UpdateControl instance From efc0f058c6b0b93a06327a9f47aadb308b67dd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 26 Nov 2021 10:20:45 +0100 Subject: [PATCH 0156/1608] refactor: rename internal package to source, moving LifecycleAware (#716) Co-authored-by: Chris Laprun --- .../io/javaoperatorsdk/operator/Operator.java | 2 +- .../api/config/ControllerConfiguration.java | 4 +- .../ControllerConfigurationOverrider.java | 2 +- .../DefaultControllerConfiguration.java | 2 +- .../reconciler/ControllerConfiguration.java | 2 +- .../reconciler/EventSourceInitializer.java | 2 +- .../operator/processing/Controller.java | 12 +--- .../{api => processing}/LifecycleAware.java | 2 +- .../processing/{ => event}/EventMarker.java | 7 +- .../{ => event}/EventProcessor.java | 70 +++++++++---------- .../processing/event/EventSourceManager.java | 50 ++++++------- .../{ => event}/ExecutionScope.java | 5 +- .../{ => event}/PostExecutionControl.java | 4 +- .../{ => event}/ReconciliationDispatcher.java | 14 ++-- .../event/internal/ResourceAction.java | 5 -- .../{ => source}/AbstractEventSource.java | 4 +- .../ControllerResourceEventSource.java | 3 +- .../event/{ => source}/EventSource.java | 6 +- .../{ => source}/EventSourceRegistry.java | 3 +- .../InformerEventSource.java | 3 +- .../event/{internal => source}/Mappers.java | 2 +- .../OnceWhitelistEventFilterEventFilter.java | 2 +- .../event/source/ResourceAction.java | 5 ++ .../{internal => source}/ResourceEvent.java | 2 +- .../ResourceEventFilter.java | 2 +- .../ResourceEventFilters.java | 2 +- .../TimerEventSource.java | 3 +- .../{ => event}/EventMarkerTest.java | 4 +- .../{ => event}/EventProcessorTest.java | 64 +++++++++-------- .../event/EventSourceManagerTest.java | 16 ++--- .../ReconciliationDispatcherTest.java | 5 +- .../ControllerResourceEventSourceTest.java | 2 +- .../CustomResourceSelectorTest.java | 2 +- ...ceWhitelistEventFilterEventFilterTest.java | 2 +- .../ResourceEventFilterTest.java | 2 +- .../TimerEventSourceTest.java | 2 +- .../runtime/AnnotationConfiguration.java | 4 +- ...formerEventSourceTestCustomReconciler.java | 6 +- .../operator/sample/TomcatReconciler.java | 4 +- .../operator/sample/WebappReconciler.java | 6 +- 40 files changed, 158 insertions(+), 181 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api => processing}/LifecycleAware.java (78%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/{ => event}/EventMarker.java (93%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/{ => event}/EventProcessor.java (86%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/{ => event}/ExecutionScope.java (82%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/{ => event}/PostExecutionControl.java (95%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/{ => event}/ReconciliationDispatcher.java (96%) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceAction.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{ => source}/AbstractEventSource.java (64%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/ControllerResourceEventSource.java (98%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{ => source}/EventSource.java (56%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{ => source}/EventSourceRegistry.java (84%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/InformerEventSource.java (97%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/Mappers.java (96%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/OnceWhitelistEventFilterEventFilter.java (94%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceAction.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/ResourceEvent.java (89%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/ResourceEventFilter.java (97%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/ResourceEventFilters.java (99%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/TimerEventSource.java (94%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/{ => event}/EventMarkerTest.java (94%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/{ => event}/EventProcessorTest.java (85%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/{ => event}/ReconciliationDispatcherTest.java (98%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/ControllerResourceEventSourceTest.java (99%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/CustomResourceSelectorTest.java (99%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/OnceWhitelistEventFilterEventFilterTest.java (94%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/ResourceEventFilterTest.java (99%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/{internal => source}/TimerEventSourceTest.java (98%) 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 2c14f4da8c..d860a1eb7e 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 @@ -13,12 +13,12 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Version; -import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.LifecycleAware; @SuppressWarnings("rawtypes") public class Operator implements AutoCloseable, LifecycleAware { 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 b729ce2d07..dd5eb30cc4 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 @@ -7,8 +7,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ControllerUtils; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilters; public interface ControllerConfiguration { 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 31be7cb983..515addcad0 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 @@ -5,7 +5,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; public class ControllerConfigurationOverrider { 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 index 5e8f2611bc..965ac32f76 100644 --- 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 @@ -4,7 +4,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; public class DefaultControllerConfiguration implements ControllerConfiguration { 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 3a522c1347..7abd5f7b43 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,7 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 5a23e0da16..03eaf2b062 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.api.reconciler; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; public interface EventSourceInitializer { 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 ff6ace1b87..1bbb48873c 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 @@ -11,7 +11,6 @@ import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -20,7 +19,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { @@ -28,7 +27,6 @@ public class Controller implements Reconciler, private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; private EventSourceManager eventSourceManager; - private EventProcessor eventProcessor; public Controller(Reconciler reconciler, ControllerConfiguration configuration, @@ -170,10 +168,6 @@ public void start() throws OperatorException { } eventSourceManager = new EventSourceManager<>(this); - eventProcessor = - new EventProcessor<>(this, eventSourceManager.getControllerResourceEventSource()); - eventProcessor.setEventSourceManager(eventSourceManager); - eventSourceManager.setEventProcessor(eventProcessor); if (reconciler instanceof EventSourceInitializer) { ((EventSourceInitializer) reconciler).prepareEventSources(eventSourceManager); } @@ -183,7 +177,6 @@ public void start() throws OperatorException { + controllerName + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } - eventProcessor.start(); eventSourceManager.start(); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); @@ -223,8 +216,5 @@ public void stop() { if (eventSourceManager != null) { eventSourceManager.stop(); } - if (eventProcessor != null) { - eventProcessor.stop(); - } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/LifecycleAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/LifecycleAware.java similarity index 78% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/LifecycleAware.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/LifecycleAware.java index 320368bcea..d96391dcd5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/LifecycleAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/LifecycleAware.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api; +package io.javaoperatorsdk.operator.processing; import io.javaoperatorsdk.operator.OperatorException; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventMarker.java similarity index 93% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventMarker.java index b32983ac8e..fb44c933e5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventMarker.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventMarker.java @@ -1,10 +1,7 @@ -package io.javaoperatorsdk.operator.processing; +package io.javaoperatorsdk.operator.processing.event; import java.util.HashMap; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.ResourceID; - /** * Manages the state of received events. Basically there can be only three distinct states relevant * for event processing. Either an event is received, so we eventually process or no event for @@ -13,7 +10,7 @@ * events are irrelevant for us from this point. Note that the dependant resources are either * cleaned up by K8S garbage collection or by the controller implementation for cleanup. */ -public class EventMarker { +class EventMarker { public enum EventingState { /** Event but NOT Delete event present */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java similarity index 86% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java index 360001602f..b51c517cd2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/EventProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing; +package io.javaoperatorsdk.operator.processing.event; import java.util.HashMap; import java.util.HashSet; @@ -14,17 +14,16 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEvent; +import io.javaoperatorsdk.operator.processing.LifecycleAware; +import io.javaoperatorsdk.operator.processing.MDCUtils; +import io.javaoperatorsdk.operator.processing.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEvent; +import io.javaoperatorsdk.operator.processing.event.source.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.processing.retry.Retry; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; @@ -36,8 +35,7 @@ * Event handler that makes sure that events are processed in a "single threaded" way per resource * UID, while buffering events which are received during an execution. */ -public class EventProcessor - implements EventHandler, LifecycleAware { +class EventProcessor implements EventHandler, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventProcessor.class); @@ -51,32 +49,34 @@ public class EventProcessor private final Metrics metrics; private volatile boolean running; private final ResourceCache resourceCache; - private EventSourceManager eventSourceManager; + private final EventSourceManager eventSourceManager; private final EventMarker eventMarker; - public EventProcessor(Controller controller, ResourceCache resourceCache) { + EventProcessor(EventSourceManager eventSourceManager) { this( - resourceCache, + eventSourceManager.getControllerResourceEventSource(), ExecutorServiceManager.instance().executorService(), - controller.getConfiguration().getName(), - new ReconciliationDispatcher<>(controller), - GenericRetry.fromConfiguration(controller.getConfiguration().getRetryConfiguration()), - controller.getConfiguration().getConfigurationService().getMetrics(), - new EventMarker()); + eventSourceManager.getController().getConfiguration().getName(), + new ReconciliationDispatcher<>(eventSourceManager.getController()), + GenericRetry.fromConfiguration( + eventSourceManager.getController().getConfiguration().getRetryConfiguration()), + eventSourceManager.getController().getConfiguration().getConfigurationService() + .getMetrics(), + eventSourceManager); } EventProcessor(ReconciliationDispatcher reconciliationDispatcher, - ResourceCache resourceCache, + EventSourceManager eventSourceManager, String relatedControllerName, - Retry retry, EventMarker eventMarker) { - this(resourceCache, null, relatedControllerName, reconciliationDispatcher, retry, null, - eventMarker); + Retry retry) { + this(eventSourceManager.getControllerResourceEventSource(), null, relatedControllerName, + reconciliationDispatcher, retry, null, eventSourceManager); } private EventProcessor(ResourceCache resourceCache, ExecutorService executor, String relatedControllerName, ReconciliationDispatcher reconciliationDispatcher, Retry retry, Metrics metrics, - EventMarker eventMarker) { + EventSourceManager eventSourceManager) { this.running = true; this.executor = executor == null @@ -88,11 +88,12 @@ private EventProcessor(ResourceCache resourceCache, ExecutorService executor, this.retry = retry; this.resourceCache = resourceCache; this.metrics = metrics != null ? metrics : Metrics.NOOP; - this.eventMarker = eventMarker; + this.eventMarker = new EventMarker(); + this.eventSourceManager = eventSourceManager; } - public void setEventSourceManager(EventSourceManager eventSourceManager) { - this.eventSourceManager = eventSourceManager; + EventMarker getEventMarker() { + return eventMarker; } @Override @@ -243,9 +244,12 @@ private boolean isCacheReadyForInstantReconciliation(ExecutionScope execution private void reScheduleExecutionIfInstructed(PostExecutionControl postExecutionControl, R customResource) { - postExecutionControl.getReScheduleDelay().ifPresent(delay -> eventSourceManager - .getRetryAndRescheduleTimerEventSource() - .scheduleOnce(customResource, delay)); + postExecutionControl.getReScheduleDelay() + .ifPresent(delay -> retryEventSource().scheduleOnce(customResource, delay)); + } + + TimerEventSource retryEventSource() { + return eventSourceManager.retryEventSource(); } /** @@ -275,9 +279,7 @@ private void handleRetryOnException(ExecutionScope executionScope, delay, customResourceID); metrics.failedReconciliation(customResourceID, exception); - eventSourceManager - .getRetryAndRescheduleTimerEventSource() - .scheduleOnce(executionScope.getResource(), delay); + retryEventSource().scheduleOnce(executionScope.getResource(), delay); }, () -> log.error("Exhausted retries for {}", executionScope)); } @@ -289,9 +291,7 @@ private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) { if (isRetryConfigured()) { retryState.remove(executionScope.getCustomResourceID()); } - eventSourceManager - .getRetryAndRescheduleTimerEventSource() - .cancelOnceSchedule(executionScope.getCustomResourceID()); + retryEventSource().cancelOnceSchedule(executionScope.getCustomResourceID()); } private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) { 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 760bde2f43..64333f8981 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 @@ -12,11 +12,12 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.LifecycleAware; import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.processing.EventProcessor; -import io.javaoperatorsdk.operator.processing.event.internal.ControllerResourceEventSource; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; +import io.javaoperatorsdk.operator.processing.LifecycleAware; +import io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.TimerEventSource; public class EventSourceManager implements EventSourceRegistry, LifecycleAware { @@ -25,38 +26,33 @@ public class EventSourceManager private final ReentrantLock lock = new ReentrantLock(); private final Set eventSources = Collections.synchronizedSet(new HashSet<>()); - private EventProcessor eventProcessor; + private final EventProcessor eventProcessor; private TimerEventSource retryAndRescheduleTimerEventSource; private ControllerResourceEventSource controllerResourceEventSource; + private final Controller controller; - EventSourceManager() { - init(); + EventSourceManager(EventProcessor eventProcessor) { + this.eventProcessor = eventProcessor; + controller = null; + initRetryEventSource(); } public EventSourceManager(Controller controller) { - init(); + this.controller = controller; controllerResourceEventSource = new ControllerResourceEventSource<>(controller); + this.eventProcessor = new EventProcessor<>(this); registerEventSource(controllerResourceEventSource); + initRetryEventSource(); } - private void init() { - this.retryAndRescheduleTimerEventSource = new TimerEventSource<>(); + private void initRetryEventSource() { + retryAndRescheduleTimerEventSource = new TimerEventSource<>(); registerEventSource(retryAndRescheduleTimerEventSource); } - public EventSourceManager setEventProcessor(EventProcessor eventProcessor) { - this.eventProcessor = eventProcessor; - if (controllerResourceEventSource != null) { - controllerResourceEventSource.setEventHandler(eventProcessor); - } - if (retryAndRescheduleTimerEventSource != null) { - retryAndRescheduleTimerEventSource.setEventHandler(eventProcessor); - } - return this; - } - @Override public void start() throws OperatorException { + eventProcessor.start(); lock.lock(); try { log.debug("Starting event sources."); @@ -88,6 +84,7 @@ public void stop() { } finally { lock.unlock(); } + eventProcessor.stop(); } @Override @@ -121,10 +118,6 @@ public void cleanupForCustomResource(ResourceID customResourceUid) { } } - public TimerEventSource getRetryAndRescheduleTimerEventSource() { - return retryAndRescheduleTimerEventSource; - } - @Override public Set getRegisteredEventSources() { return Collections.unmodifiableSet(eventSources); @@ -135,4 +128,11 @@ public ControllerResourceEventSource getControllerResourceEventSource() { return controllerResourceEventSource; } + TimerEventSource retryEventSource() { + return retryAndRescheduleTimerEventSource; + } + + Controller getController() { + return controller; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java similarity index 82% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java index cb40ab35eb..037e3c4f5f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ExecutionScope.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExecutionScope.java @@ -1,10 +1,9 @@ -package io.javaoperatorsdk.operator.processing; +package io.javaoperatorsdk.operator.processing.event; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -public class ExecutionScope { +class ExecutionScope { // the latest custom resource from cache private final R resource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java similarity index 95% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java index e8df3225ef..5d9a623e20 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java @@ -1,10 +1,10 @@ -package io.javaoperatorsdk.operator.processing; +package io.javaoperatorsdk.operator.processing.event; import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -public final class PostExecutionControl { +final class PostExecutionControl { private final boolean onlyFinalizerHandled; private final R updatedCustomResource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java similarity index 96% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java index a7d55c0ae3..01f6de14ac 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing; +package io.javaoperatorsdk.operator.processing.event; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +18,7 @@ import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.Controller; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; @@ -26,7 +27,7 @@ /** * Handles calls and results of a Reconciler and finalizer related logic */ -public class ReconciliationDispatcher { +class ReconciliationDispatcher { private static final Logger log = LoggerFactory.getLogger(ReconciliationDispatcher.class); @@ -129,8 +130,7 @@ private PostExecutionControl handleReconcile( private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context) || shouldUpdateObservedGenerationAutomatically(resource)) { - return controller.getConfiguration().getConfigurationService().getResourceCloner() - .clone(resource); + return configuration().getConfigurationService().getResourceCloner().clone(resource); } else { return resource; } @@ -192,8 +192,7 @@ private R updateStatusGenerationAware(R resource) { } private boolean shouldUpdateObservedGenerationAutomatically(R resource) { - if (controller.getConfiguration().isGenerationAware() - && resource instanceof CustomResource) { + if (configuration().isGenerationAware() && resource instanceof CustomResource) { var customResource = (CustomResource) resource; var status = customResource.getStatus(); // Note that if status is null we won't update the observed generation. @@ -207,8 +206,7 @@ private boolean shouldUpdateObservedGenerationAutomatically(R resource) { } private void updateStatusObservedGenerationIfRequired(R resource) { - if (controller.getConfiguration().isGenerationAware() - && resource instanceof CustomResource) { + if (configuration().isGenerationAware() && resource instanceof CustomResource) { var customResource = (CustomResource) resource; var status = customResource.getStatus(); // Note that if status is null we won't update the observed generation. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceAction.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceAction.java deleted file mode 100644 index 6fbc233133..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceAction.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.internal; - -public enum ResourceAction { - ADDED, UPDATED, DELETED -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java similarity index 64% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java index 8d9984db14..555da0d1b1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/AbstractEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java @@ -1,4 +1,6 @@ -package io.javaoperatorsdk.operator.processing.event; +package io.javaoperatorsdk.operator.processing.event.source; + +import io.javaoperatorsdk.operator.processing.event.EventHandler; public abstract class AbstractEventSource implements EventSource { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java similarity index 98% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java index 775fd06948..0729dde5ef 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -19,7 +19,6 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.ResourceCache; -import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java similarity index 56% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java index a2be4547a2..e0d0bc45e1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java @@ -1,6 +1,8 @@ -package io.javaoperatorsdk.operator.processing.event; +package io.javaoperatorsdk.operator.processing.event.source; -import io.javaoperatorsdk.operator.api.LifecycleAware; +import io.javaoperatorsdk.operator.processing.LifecycleAware; +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; public interface EventSource extends LifecycleAware { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java similarity index 84% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java index 518b470fa8..a2d38690cc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRegistry.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java @@ -1,10 +1,9 @@ -package io.javaoperatorsdk.operator.processing.event; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.processing.event.internal.ControllerResourceEventSource; public interface EventSourceRegistry { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSource.java similarity index 97% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSource.java index 8758566619..3bdc8fb764 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.Objects; import java.util.Set; @@ -13,7 +13,6 @@ import io.fabric8.kubernetes.client.informers.SharedInformer; import io.fabric8.kubernetes.client.informers.cache.Cache; import io.fabric8.kubernetes.client.informers.cache.Store; -import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Mappers.java similarity index 96% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Mappers.java index 07d3b05cfe..d428bb26cc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Mappers.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.Collections; import java.util.Set; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilter.java similarity index 94% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilter.java index 34c5514c39..aba822dcf6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilter.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceAction.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceAction.java new file mode 100644 index 0000000000..ff0b4673be --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceAction.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +public enum ResourceAction { + ADDED, UPDATED, DELETED +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEvent.java similarity index 89% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEvent.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEvent.java index c15792f867..7610a880b7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEvent.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilter.java similarity index 97% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilter.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilter.java index b4c101be00..01f7e1aec0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilter.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilters.java similarity index 99% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilters.java index b7d69408ec..9d90ebd963 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilters.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSource.java similarity index 94% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSource.java index 030b46298e..ebac87cdf2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.Map; import java.util.Timer; @@ -10,7 +10,6 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventMarkerTest.java similarity index 94% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventMarkerTest.java index 08090f3c1d..b6fec2b80c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventMarkerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventMarkerTest.java @@ -1,10 +1,8 @@ -package io.javaoperatorsdk.operator.processing; +package io.javaoperatorsdk.operator.processing.event; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.javaoperatorsdk.operator.processing.event.ResourceID; - import static org.assertj.core.api.Assertions.assertThat; class EventMarkerTest { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java similarity index 85% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index 2e3c708c6c..4700b1f4d2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/EventProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing; +package io.javaoperatorsdk.operator.processing.event; import java.util.List; import java.util.Optional; @@ -12,21 +12,25 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.Event; -import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.internal.ControllerResourceEventSource; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceAction; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEvent; -import io.javaoperatorsdk.operator.processing.event.internal.TimerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEvent; +import io.javaoperatorsdk.operator.processing.event.source.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; -import static io.javaoperatorsdk.operator.processing.event.internal.ResourceAction.DELETED; +import static io.javaoperatorsdk.operator.processing.event.source.ResourceAction.DELETED; import static org.assertj.core.api.Assertions.assertThat; 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.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class EventProcessorTest { @@ -35,29 +39,27 @@ class EventProcessorTest { public static final int FAKE_CONTROLLER_EXECUTION_DURATION = 250; public static final int SEPARATE_EXECUTION_TIMEOUT = 450; public static final String TEST_NAMESPACE = "default-event-handler-test"; - private EventMarker eventMarker = new EventMarker(); private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); - private EventSourceManager eventSourceManagerMock = - mock(EventSourceManager.class); - private ResourceCache resourceCacheMock = mock(ResourceCache.class); - + private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); + private ControllerResourceEventSource resourceCacheMock = + mock(ControllerResourceEventSource.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); - - private EventProcessor eventProcessor = - new EventProcessor(reconciliationDispatcherMock, resourceCacheMock, "Test", null, - eventMarker); - - private EventProcessor eventProcessorWithRetry = - new EventProcessor(reconciliationDispatcherMock, resourceCacheMock, "Test", - GenericRetry.defaultLimitedExponentialRetry(), eventMarker); + private EventProcessor eventProcessor; + private EventProcessor eventProcessorWithRetry; @BeforeEach public void setup() { - when(eventSourceManagerMock.getRetryAndRescheduleTimerEventSource()) - .thenReturn(retryTimerEventSourceMock); - eventProcessor.setEventSourceManager(eventSourceManagerMock); - eventProcessorWithRetry.setEventSourceManager(eventSourceManagerMock); + when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(resourceCacheMock); + + eventProcessor = + spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null)); + eventProcessorWithRetry = + spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", + GenericRetry.defaultLimitedExponentialRetry())); + + when(eventProcessor.retryEventSource()).thenReturn(retryTimerEventSourceMock); + when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); } @Test @@ -217,7 +219,7 @@ public void cleansUpWhenDeleteEventReceivedAndNoEventPresent() { public void cleansUpAfterExecutionIfOnlyDeleteEventMarkLeft() { var cr = testCustomResource(); var crEvent = prepareCREvent(ResourceID.fromResource(cr)); - eventMarker.markDeleteEventReceived(crEvent.getRelatedCustomResourceID()); + eventProcessor.getEventMarker().markDeleteEventReceived(crEvent.getRelatedCustomResourceID()); var executionScope = new ExecutionScope(cr, null); eventProcessor.eventProcessingFinished(executionScope, @@ -234,7 +236,7 @@ public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { var updatedCr = testCustomResource(crID); updatedCr.getMetadata().setResourceVersion("2"); var mockCREventSource = mock(ControllerResourceEventSource.class); - eventMarker.markEventReceived(crID); + eventProcessor.getEventMarker().markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(mockCREventSource); @@ -254,7 +256,7 @@ public void dontWhitelistsEventWhenOtherChangeDuringExecution() { var otherChangeCR = testCustomResource(crID); otherChangeCR.getMetadata().setResourceVersion("3"); var mockCREventSource = mock(ControllerResourceEventSource.class); - eventMarker.markEventReceived(crID); + eventProcessor.getEventMarker().markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(otherChangeCR)); when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(mockCREventSource); @@ -270,7 +272,7 @@ public void dontWhitelistsEventIfUpdatedEventInCache() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); var mockCREventSource = mock(ControllerResourceEventSource.class); - eventMarker.markEventReceived(crID); + eventProcessor.getEventMarker().markEventReceived(crID); when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(mockCREventSource); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index 2054efd10b..164a66b970 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -3,12 +3,11 @@ import java.io.IOException; import java.util.Set; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.processing.EventProcessor; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.eq; @@ -19,13 +18,7 @@ class EventSourceManagerTest { private EventProcessor eventProcessorMock = mock(EventProcessor.class); - private EventSourceManager eventSourceManager = - new EventSourceManager(); - - @BeforeEach - public void setup() { - eventSourceManager.setEventProcessor(eventProcessorMock); - } + private EventSourceManager eventSourceManager = new EventSourceManager(eventProcessorMock); @Test public void registersEventSource() { @@ -33,9 +26,8 @@ public void registersEventSource() { eventSourceManager.registerEventSource(eventSource); - Set registeredSources = - eventSourceManager.getRegisteredEventSources(); - assertThat(registeredSources).hasSize(2); + Set registeredSources = eventSourceManager.getRegisteredEventSources(); + assertThat(registeredSources).contains(eventSource); verify(eventSource, times(1)).setEventHandler(eq(eventProcessorMock)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java similarity index 98% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 16e5bf4b87..db4736697a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ReconciliationDispatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing; +package io.javaoperatorsdk.operator.processing.event; import java.util.ArrayList; @@ -15,7 +15,8 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.ReconciliationDispatcher.CustomResourceFacade; +import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.CustomResourceFacade; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSourceTest.java similarity index 99% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSourceTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSourceTest.java index 5fb4b65dfb..e63c415e20 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSourceTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.time.LocalDateTime; import java.util.List; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java similarity index 99% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index 14218b9398..22be8649e6 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.Date; import java.util.Set; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java similarity index 94% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java index 5634f11cad..4595f1b649 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/OnceWhitelistEventFilterEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import org.junit.jupiter.api.Test; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java similarity index 99% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilterTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java index 1c130a0f6b..571d9433c6 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/ResourceEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilterTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.List; import java.util.Objects; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSourceTest.java similarity index 98% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSourceTest.java index e41c78ba55..d1768a8547 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/internal/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSourceTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.internal; +package io.javaoperatorsdk.operator.processing.event.source; import java.io.IOException; import java.util.List; diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index a56ee66cb0..976c058c26 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -8,8 +8,8 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.internal.ResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilters; public class AnnotationConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 242498575a..71463e13d0 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -11,9 +11,9 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; -import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; -import io.javaoperatorsdk.operator.processing.event.internal.Mappers; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.Mappers; import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 65e4030118..64118be4aa 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -18,9 +18,9 @@ import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.InformerEventSource; import static java.util.Collections.EMPTY_SET; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 9b9b566061..4b1bb6ee33 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -18,10 +18,10 @@ import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.internal.ControllerResourceEventSource; -import io.javaoperatorsdk.operator.processing.event.internal.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.InformerEventSource; import okhttp3.Response; From b3c04b834db10323e7c1b7795d03df27428a3576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 26 Nov 2021 12:10:31 +0100 Subject: [PATCH 0157/1608] V2 migration guide (#717) --- README.md | 3 ++ docs/_data/sidebar.yml | 2 + docs/documentation/patterns-best-practices.md | 4 +- docs/documentation/use-samples.md | 2 +- docs/documentation/v2-migration.md | 53 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 docs/documentation/v2-migration.md diff --git a/README.md b/README.md index b211f563d8..1aba79d50c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ by [operator-sdk](https://github.com/operator-framework/operator-sdk). Our webpage with documentation is getting better every day: https://javaoperatorsdk.io/ +**!! NOTE the main branch now contains source code for v2, which is under development, for actual +version see [v1 branch](https://github.com/java-operator-sdk/java-operator-sdk/tree/v1) !!** + Table of Contents ========== diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index a9a43907a2..4963e97be1 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -13,4 +13,6 @@ url: /docs/faq - title: Contributing url: /docs/contributing + - title: Migrating from v1 to v2 + url: /docs/v2-migration diff --git a/docs/documentation/patterns-best-practices.md b/docs/documentation/patterns-best-practices.md index c5103de3ff..13de8ae910 100644 --- a/docs/documentation/patterns-best-practices.md +++ b/docs/documentation/patterns-best-practices.md @@ -12,10 +12,10 @@ of Java Operator SDK. ## Implementing a Controller -### Sync of Async Way of Resource Handling - ### Idempotency +### Sync of Async Way of Resource Handling + ## Why to Have Automated Retries? ## Managing State diff --git a/docs/documentation/use-samples.md b/docs/documentation/use-samples.md index c4d6f99533..04988c076e 100644 --- a/docs/documentation/use-samples.md +++ b/docs/documentation/use-samples.md @@ -58,7 +58,7 @@ The Controller implements the business logic and describes all the classes neede ```java -@Controller +@ControllerConfiguration public class WebServerController implements Reconciler { // Return the changed resource, so it gets updated. See javadoc for details. diff --git a/docs/documentation/v2-migration.md b/docs/documentation/v2-migration.md new file mode 100644 index 0000000000..5a63a7b9ca --- /dev/null +++ b/docs/documentation/v2-migration.md @@ -0,0 +1,53 @@ +--- +title: Migrating from v1 to v2 +description: Migrating from v1 to v2 +layout: docs +permalink: /docs/v2-migration +--- + +# Migrating from v1 to v2 + +Version 2 of the framework introduces improvements, features and breaking changes for the APIs both internal and user +facing ones. The migration should be however trivial in most of the cases. For detailed overview of all major issues +until the release of +v`2.0.0` [see milestone on GitHub](https://github.com/java-operator-sdk/java-operator-sdk/milestone/1). For a summary +and reasoning behind some naming changes +see [this issue](https://github.com/java-operator-sdk/java-operator-sdk/issues/655) + +## User Facing API Changes + +The following items are renamed and slightly changed: + +- [`ResourceController`](https://github.com/java-operator-sdk/java-operator-sdk/blob/v1/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ResourceController.java) + interface is renamed + to [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) + . In addition, methods: + - `createOrUpdate` renamed to `reconcile` + - `delete` renamed to `cleanup` +- Events are removed from + the [`Context`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java) + of `Reconciler` methods . The rationale behind this, is that there is a consensus now on the pattern that the events + should not be used to implement a reconciliation logic. +- The `init` method is extracted from `ResourceController` / `Reconciler` to a separate interface + called [EventSourceInitializer](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java) + that `Reconcile` should implement in order to register event sources. See + also [sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java) + for usage. Here also + the [`EventSourceManager`](https://github.com/java-operator-sdk/java-operator-sdk/blob/v1/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java) + is renamed + to [`EventSourceRegistry`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java) + , and it's interface refined. +- [`@Controller`](https://github.com/java-operator-sdk/java-operator-sdk/blob/v1/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java) + annotation renamed + to [`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java) + +### Event Sources + +- Addressing resources within event sources (and in the framework internally) is now changed from `.metadata.uid` to a + pair of `.metadata.name` and optional `.metadata.namespace` of resource. Represented + by [`ResourceID.`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java) +- The [`Event`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java) + API is simplified. Now if an event source produces an event it needs to just produce an instance of this class. +- [`EventSource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java) + is refactored, but the changes are trivial. + From 212ee19866fa1f40511519e478e24ab53e1d5af9 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 26 Nov 2021 12:17:25 +0100 Subject: [PATCH 0158/1608] docs: method naming fix in docs --- docs/documentation/v2-migration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/documentation/v2-migration.md b/docs/documentation/v2-migration.md index 5a63a7b9ca..fe31fabaf5 100644 --- a/docs/documentation/v2-migration.md +++ b/docs/documentation/v2-migration.md @@ -22,8 +22,8 @@ The following items are renamed and slightly changed: interface is renamed to [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) . In addition, methods: - - `createOrUpdate` renamed to `reconcile` - - `delete` renamed to `cleanup` + - `createOrUpdateResource` renamed to `reconcile` + - `deleteResource` renamed to `cleanup` - Events are removed from the [`Context`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java) of `Reconciler` methods . The rationale behind this, is that there is a consensus now on the pattern that the events From 625b86edc4dd980f36df6014a9ee11edbcdc23f7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 26 Nov 2021 11:39:57 +0000 Subject: [PATCH 0159/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 2866f42257..fc055ea220 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index af0737bf29..41ecb0f137 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 7a9977ded0..0339129c75 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 2fe67f8716..0c05632099 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 1517bd034c..809c25752c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 5d9a497cec..878c4a81df 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index ae8605f305..393d6f7563 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 34286abadc..7a6c711df5 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index ed1884aecd..0038730ea3 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 566fbd98be..2d1ad2923a 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index d1a2de4c2b..9fb7f1c92c 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index de64a684e5..cbdcabe77a 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 2a5a013764c3cda10aa633fe9e4058c8f66843e0 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 26 Nov 2021 12:47:18 +0100 Subject: [PATCH 0160/1608] fix: setting mvn version back to 2.0.0-SNAPSHOT after M1 release --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index fc055ea220..2866f42257 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 41ecb0f137..af0737bf29 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 0339129c75..7a9977ded0 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 0c05632099..2fe67f8716 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 809c25752c..1517bd034c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 878c4a81df..5d9a497cec 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 393d6f7563..ae8605f305 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 7a6c711df5..34286abadc 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 0038730ea3..ed1884aecd 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 2d1ad2923a..566fbd98be 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 9fb7f1c92c..d1a2de4c2b 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index cbdcabe77a..de64a684e5 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From d537da2b738038951d64ff4123f91151b985d894 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 26 Nov 2021 13:58:39 +0100 Subject: [PATCH 0161/1608] refactor: initialize EventMarker at declaration point --- .../operator/processing/event/EventProcessor.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 b51c517cd2..bda71502bb 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 @@ -50,7 +50,7 @@ class EventProcessor implements EventHandler, LifecycleAw private volatile boolean running; private final ResourceCache resourceCache; private final EventSourceManager eventSourceManager; - private final EventMarker eventMarker; + private final EventMarker eventMarker = new EventMarker(); EventProcessor(EventSourceManager eventSourceManager) { this( @@ -88,7 +88,6 @@ private EventProcessor(ResourceCache resourceCache, ExecutorService executor, this.retry = retry; this.resourceCache = resourceCache; this.metrics = metrics != null ? metrics : Metrics.NOOP; - this.eventMarker = new EventMarker(); this.eventSourceManager = eventSourceManager; } From 8626ab41f551b115ea8733bde5c29b9e3e24e4de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 08:40:31 +0100 Subject: [PATCH 0162/1608] chore(deps): bump junit-bom from 5.8.1 to 5.8.2 (#718) Bumps [junit-bom](https://github.com/junit-team/junit5) from 5.8.1 to 5.8.2. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.8.1...r5.8.2) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1517bd034c..dba85b7d19 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ ${java.version} ${java.version} - 5.8.1 + 5.8.2 5.10.1 1.7.32 2.14.1 From 80ff2449ced3f6cb3db83cf121552e6b867a5960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 29 Nov 2021 10:26:14 +0100 Subject: [PATCH 0163/1608] Improve cache interface for CustomResourceEventSource (#684) Co-authored-by: Chris Laprun --- .../operator/processing/MDCUtils.java | 8 +-- .../operator/processing/ResourceCache.java | 12 ---- .../processing/event/EventProcessor.java | 56 +++++++--------- .../event/source/ControllerResourceCache.java | 67 +++++++++++++++++++ .../source/ControllerResourceEventSource.java | 40 +++++------ .../event/source/ResourceCache.java | 27 ++++++++ .../processing/event/EventProcessorTest.java | 36 +++++----- .../operator/InformerEventSourceIT.java | 2 +- .../operator/sample/TomcatReconciler.java | 1 + .../operator/sample/WebappReconciler.java | 21 +++--- 10 files changed, 166 insertions(+), 104 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceCache.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java index 6f475b56d8..4578c31744 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java @@ -15,17 +15,17 @@ public class MDCUtils { private static final String GENERATION = "resource.generation"; private static final String UID = "resource.uid"; - public static void addCustomResourceIDInfo(ResourceID resourceID) { + public static void addResourceIDInfo(ResourceID resourceID) { MDC.put(NAME, resourceID.getName()); MDC.put(NAMESPACE, resourceID.getNamespace().orElse("no namespace")); } - public static void removeCustomResourceIDInfo() { + public static void removeResourceIDInfo() { MDC.remove(NAME); MDC.remove(NAMESPACE); } - public static void addCustomResourceInfo(HasMetadata resource) { + public static void addResourceInfo(HasMetadata resource) { MDC.put(API_VERSION, resource.getApiVersion()); MDC.put(KIND, resource.getKind()); MDC.put(NAME, resource.getMetadata().getName()); @@ -37,7 +37,7 @@ public static void addCustomResourceInfo(HasMetadata resource) { MDC.put(UID, resource.getMetadata().getUid()); } - public static void removeCustomResourceInfo() { + public static void removeResourceInfo() { MDC.remove(API_VERSION); MDC.remove(KIND); MDC.remove(NAME); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java deleted file mode 100644 index 8fa497f61c..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceCache.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.javaoperatorsdk.operator.processing; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.ResourceID; - -public interface ResourceCache { - - Optional getCustomResource(ResourceID resourceID); - -} 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 bda71502bb..faaf21910b 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 @@ -20,8 +20,8 @@ import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.MDCUtils; -import io.javaoperatorsdk.operator.processing.ResourceCache; import io.javaoperatorsdk.operator.processing.event.source.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; import io.javaoperatorsdk.operator.processing.event.source.ResourceEvent; import io.javaoperatorsdk.operator.processing.event.source.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -54,7 +54,7 @@ class EventProcessor implements EventHandler, LifecycleAw EventProcessor(EventSourceManager eventSourceManager) { this( - eventSourceManager.getControllerResourceEventSource(), + eventSourceManager.getControllerResourceEventSource().getResourceCache(), ExecutorServiceManager.instance().executorService(), eventSourceManager.getController().getConfiguration().getName(), new ReconciliationDispatcher<>(eventSourceManager.getController()), @@ -69,7 +69,8 @@ class EventProcessor implements EventHandler, LifecycleAw EventSourceManager eventSourceManager, String relatedControllerName, Retry retry) { - this(eventSourceManager.getControllerResourceEventSource(), null, relatedControllerName, + this(eventSourceManager.getControllerResourceEventSource().getResourceCache(), null, + relatedControllerName, reconciliationDispatcher, retry, null, eventSourceManager); } @@ -105,7 +106,7 @@ public void handleEvent(Event event) { return; } final var resourceID = event.getRelatedCustomResourceID(); - MDCUtils.addCustomResourceIDInfo(resourceID); + MDCUtils.addResourceIDInfo(resourceID); metrics.receivedEvent(event); handleEventMarking(event); @@ -116,42 +117,35 @@ public void handleEvent(Event event) { } } finally { lock.unlock(); - MDCUtils.removeCustomResourceIDInfo(); + MDCUtils.removeResourceIDInfo(); } } - private void submitReconciliationExecution(ResourceID customResourceUid) { + private void submitReconciliationExecution(ResourceID resourceID) { try { - boolean controllerUnderExecution = isControllerUnderExecution(customResourceUid); - Optional latestCustomResource = - resourceCache.getCustomResource(customResourceUid); - latestCustomResource.ifPresent(MDCUtils::addCustomResourceInfo); - if (!controllerUnderExecution - && latestCustomResource.isPresent()) { - setUnderExecutionProcessing(customResourceUid); - final var retryInfo = retryInfo(customResourceUid); - ExecutionScope executionScope = - new ExecutionScope<>( - latestCustomResource.get(), - retryInfo); - eventMarker.unMarkEventReceived(customResourceUid); - metrics.reconcileCustomResource(customResourceUid, retryInfo); + boolean controllerUnderExecution = isControllerUnderExecution(resourceID); + Optional latest = resourceCache.get(resourceID); + latest.ifPresent(MDCUtils::addResourceInfo); + if (!controllerUnderExecution && latest.isPresent()) { + setUnderExecutionProcessing(resourceID); + final var retryInfo = retryInfo(resourceID); + ExecutionScope executionScope = new ExecutionScope<>(latest.get(), retryInfo); + eventMarker.unMarkEventReceived(resourceID); + metrics.reconcileCustomResource(resourceID, retryInfo); log.debug("Executing events for custom resource. Scope: {}", executionScope); executor.execute(new ControllerExecution(executionScope)); } else { log.debug( - "Skipping executing controller for resource id: {}." - + " Controller in execution: {}. Latest CustomResource present: {}", - customResourceUid, + "Skipping executing controller for resource id: {}. Controller in execution: {}. Latest Resource present: {}", + resourceID, controllerUnderExecution, - latestCustomResource.isPresent()); - if (latestCustomResource.isEmpty()) { - log.warn("no custom resource found in cache for CustomResourceID: {}", - customResourceUid); + latest.isPresent()); + if (latest.isEmpty()) { + log.warn("no custom resource found in cache for ResourceID: {}", resourceID); } } } finally { - MDCUtils.removeCustomResourceInfo(); + MDCUtils.removeResourceInfo(); } } @@ -227,7 +221,7 @@ private boolean isCacheReadyForInstantReconciliation(ExecutionScope execution .orElseThrow(() -> new IllegalStateException( "Updated custom resource must be present at this point of time"))); String cachedCustomResourceVersion = getVersion(resourceCache - .getCustomResource(executionScope.getCustomResourceID()) + .get(executionScope.getCustomResourceID()) .orElseThrow(() -> new IllegalStateException( "Cached custom resource must be present at this point"))); @@ -357,7 +351,7 @@ public void run() { final var thread = Thread.currentThread(); final var name = thread.getName(); try { - MDCUtils.addCustomResourceInfo(executionScope.getResource()); + MDCUtils.addResourceInfo(executionScope.getResource()); thread.setName("EventHandler-" + controllerName); PostExecutionControl postExecutionControl = reconciliationDispatcher.handleExecution(executionScope); @@ -365,7 +359,7 @@ public void run() { } finally { // restore original name thread.setName(name); - MDCUtils.removeCustomResourceInfo(); + MDCUtils.removeResourceInfo(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceCache.java new file mode 100644 index 0000000000..2186277e32 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceCache.java @@ -0,0 +1,67 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.fabric8.kubernetes.client.informers.cache.Cache; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +import static io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY; + +public class ControllerResourceCache implements ResourceCache { + + private final Map> sharedIndexInformers; + private final Cloner cloner; + + public ControllerResourceCache(Map> sharedIndexInformers, + Cloner cloner) { + this.sharedIndexInformers = sharedIndexInformers; + this.cloner = cloner; + } + + @Override + public Stream list(Predicate predicate) { + return sharedIndexInformers.values().stream() + .flatMap(i -> i.getStore().list().stream().filter(predicate)); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + if (isWatchingAllNamespaces()) { + final var stream = sharedIndexInformers.get(ANY_NAMESPACE_MAP_KEY).getStore().list().stream() + .filter(r -> r.getMetadata().getNamespace().equals(namespace)); + return predicate != null ? stream.filter(predicate) : stream; + } else { + final var informer = sharedIndexInformers.get(namespace); + return informer != null ? informer.getStore().list().stream().filter(predicate) + : Stream.empty(); + } + } + + @Override + public Optional get(ResourceID resourceID) { + var sharedIndexInformer = sharedIndexInformers.get(ANY_NAMESPACE_MAP_KEY); + if (sharedIndexInformer == null) { + sharedIndexInformer = + sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); + } + var resource = sharedIndexInformer.getStore() + .getByKey(Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), + resourceID.getName())); + if (resource == null) { + return Optional.empty(); + } else { + return Optional.of(cloner.clone(resource)); + } + } + + private boolean isWatchingAllNamespaces() { + return sharedIndexInformers.containsKey(ANY_NAMESPACE_MAP_KEY); + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java index 0729dde5ef..faf0d8f33b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java @@ -1,6 +1,9 @@ package io.javaoperatorsdk.operator.processing.event.source; -import java.util.*; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; @@ -12,13 +15,10 @@ import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; -import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.MissingCRDException; -import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; -import io.javaoperatorsdk.operator.processing.ResourceCache; import io.javaoperatorsdk.operator.processing.event.ResourceID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; @@ -29,7 +29,7 @@ * This is a special case since is not bound to a single custom resource */ public class ControllerResourceEventSource extends AbstractEventSource - implements ResourceEventHandler, ResourceCache { + implements ResourceEventHandler { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; @@ -41,11 +41,12 @@ public class ControllerResourceEventSource extends Abstra private final ResourceEventFilter filter; private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; - private final Cloner cloner; + private final ControllerResourceCache cache; public ControllerResourceEventSource(Controller controller) { this.controller = controller; - this.cloner = controller.getConfiguration().getConfigurationService().getResourceCloner(); + var cloner = controller.getConfiguration().getConfigurationService().getResourceCloner(); + this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), @@ -128,7 +129,7 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource try { log.debug( "Event received for resource: {}", getName(customResource)); - MDCUtils.addCustomResourceInfo(customResource); + MDCUtils.addResourceInfo(customResource); if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { eventHandler.handleEvent( new ResourceEvent(action, ResourceID.fromResource(customResource))); @@ -139,7 +140,7 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource getVersion(customResource)); } } finally { - MDCUtils.removeCustomResourceInfo(); + MDCUtils.removeResourceInfo(); } } @@ -158,24 +159,13 @@ public void onDelete(T resource, boolean b) { eventReceived(ResourceAction.DELETED, resource, null); } - @Override - public Optional getCustomResource(ResourceID resourceID) { - var sharedIndexInformer = sharedIndexInformers.get(ANY_NAMESPACE_MAP_KEY); - if (sharedIndexInformer == null) { - sharedIndexInformer = - sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); - } - var resource = sharedIndexInformer.getStore() - .getByKey(Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), - resourceID.getName())); - if (resource == null) { - return Optional.empty(); - } else { - return Optional.of(cloner.clone(resource)); - } + public Optional get(ResourceID resourceID) { + return cache.get(resourceID); } - + public ControllerResourceCache getResourceCache() { + return cache; + } /** * @return shared informers by namespace. If custom resource is not namespace scoped use diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java new file mode 100644 index 0000000000..af156e24ec --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java @@ -0,0 +1,27 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public interface ResourceCache { + Predicate TRUE = (a) -> true; + + Optional get(ResourceID resourceID); + + default Stream list() { + return list(TRUE); + } + + Stream list(Predicate predicate); + + default Stream list(String namespace) { + return list(namespace, TRUE); + } + + Stream list(String namespace, Predicate predicate); +} 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 4700b1f4d2..9f99b1bdd0 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 @@ -12,10 +12,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource; -import io.javaoperatorsdk.operator.processing.event.source.ResourceAction; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEvent; -import io.javaoperatorsdk.operator.processing.event.source.TimerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.*; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -42,15 +39,20 @@ class EventProcessorTest { private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); - private ControllerResourceEventSource resourceCacheMock = - mock(ControllerResourceEventSource.class); + private ControllerResourceCache resourceCacheMock = + mock(ControllerResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); + private ControllerResourceEventSource controllerResourceEventSourceMock = + mock(ControllerResourceEventSource.class); private EventProcessor eventProcessor; private EventProcessor eventProcessorWithRetry; @BeforeEach public void setup() { - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(resourceCacheMock); + + when(eventSourceManagerMock.getControllerResourceEventSource()) + .thenReturn(controllerResourceEventSourceMock); + when(controllerResourceEventSourceMock.getResourceCache()).thenReturn(resourceCacheMock); eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null)); @@ -72,8 +74,7 @@ public void dispatchesEventsIfNoExecutionInProgress() { @Test public void skipProcessingIfLatestCustomResourceNotInCache() { Event event = prepareCREvent(); - when(resourceCacheMock.getCustomResource(event.getRelatedCustomResourceID())) - .thenReturn(Optional.empty()); + when(resourceCacheMock.get(event.getRelatedCustomResourceID())).thenReturn(Optional.empty()); eventProcessor.handleEvent(event); @@ -237,9 +238,8 @@ public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { updatedCr.getMetadata().setResourceVersion("2"); var mockCREventSource = mock(ControllerResourceEventSource.class); eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()) - .thenReturn(mockCREventSource); + when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); + when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(updatedCr)); @@ -257,9 +257,8 @@ public void dontWhitelistsEventWhenOtherChangeDuringExecution() { otherChangeCR.getMetadata().setResourceVersion("3"); var mockCREventSource = mock(ControllerResourceEventSource.class); eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(otherChangeCR)); - when(eventSourceManagerMock.getControllerResourceEventSource()) - .thenReturn(mockCREventSource); + when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(otherChangeCR)); + when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(updatedCr)); @@ -273,9 +272,8 @@ public void dontWhitelistsEventIfUpdatedEventInCache() { var cr = testCustomResource(crID); var mockCREventSource = mock(ControllerResourceEventSource.class); eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.getCustomResource(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()) - .thenReturn(mockCREventSource); + when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); + when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), PostExecutionControl.customResourceUpdated(cr)); @@ -312,7 +310,7 @@ private ResourceEvent prepareCREvent() { private ResourceEvent prepareCREvent(ResourceID uid) { TestCustomResource customResource = testCustomResource(uid); - when(resourceCacheMock.getCustomResource(eq(uid))).thenReturn(Optional.of(customResource)); + when(resourceCacheMock.get(eq(uid))).thenReturn(Optional.of(customResource)); return new ResourceEvent(ResourceAction.UPDATED, ResourceID.fromResource(customResource)); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 07a9348191..06a1238bb5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -40,7 +40,7 @@ public void testUsingInformerToWatchChangesOfConfigMap() { waitForCRStatusValue(INITIAL_STATUS_MESSAGE); configMap.getData().put(TARGET_CONFIG_MAP_KEY, UPDATE_STATUS_MESSAGE); - operator.replace(ConfigMap.class, configMap); + configMap = operator.replace(ConfigMap.class, configMap); waitForCRStatusValue(UPDATE_STATUS_MESSAGE); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 64118be4aa..ed6faa2e1c 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -17,6 +17,7 @@ import io.fabric8.kubernetes.client.dsl.ServiceResource; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 4b1bb6ee33..f536d87013 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -17,9 +17,13 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; -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.DeleteControl; +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.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.source.InformerEventSource; @@ -44,17 +48,10 @@ public void prepareEventSources(EventSourceRegistry eventSourceRegistry) // we need to find which WebApp this Tomcat custom resource is related to. // To find the related customResourceId of the WebApp resource we traverse the cache to // and identify it based on naming convention. - var webAppInformer = - eventSourceRegistry.getControllerResourceEventSource() - .getInformer(ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY); - - var ids = webAppInformer.getStore().list().stream() - .filter( - (Webapp webApp) -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) - .map(webapp -> new ResourceID(webapp.getMetadata().getName(), - webapp.getMetadata().getNamespace())) + return eventSourceRegistry.getControllerResourceEventSource().getResourceCache() + .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) + .map(ResourceID::fromResource) .collect(Collectors.toSet()); - return ids; }); eventSourceRegistry.registerEventSource(tomcatEventSource); } From a3b16119272f6fa05ca294a07a8e65f55accaf2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Nov 2021 08:23:32 +0100 Subject: [PATCH 0164/1608] chore(deps): bump spring-boot.version from 2.6.0 to 2.6.1 (#721) Bumps `spring-boot.version` from 2.6.0 to 2.6.1. Updates `spring-boot-dependencies` from 2.6.0 to 2.6.1 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.0...v2.6.1) Updates `spring-boot-maven-plugin` from 2.6.0 to 2.6.1 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.0...v2.6.1) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dba85b7d19..4478c0d50e 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 1.13.0 3.21.0 4.1.1 - 2.6.0 + 2.6.1 1.8.0 2.11 From bd3ccbe467cad5c1831bde62a2594710e9e7f280 Mon Sep 17 00:00:00 2001 From: Kate Stanley <11195226+katheris@users.noreply.github.com> Date: Tue, 30 Nov 2021 11:33:44 +0000 Subject: [PATCH 0165/1608] fix: Update examples in README to have consistent class names (#723) Signed-off-by: Katherine Stanley <11195226+katheris@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1aba79d50c..4ea7d5ade3 100644 --- a/README.md +++ b/README.md @@ -142,11 +142,11 @@ The Controller implements the business logic and describes all the classes neede ```java @ControllerConfiguration -public class WebServerController implements Reconciler { +public class WebPageReconciler implements Reconciler { // Return the changed resource, so it gets updated. See javadoc for details. @Override - public UpdateControl reconcile(CustomService resource, + public UpdateControl reconcile(WebPage resource, Context context) { // ... your logic ... return UpdateControl.updateStatus(resource); From 9871ade17f0448d012dbeee210546d82effc67ac Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 30 Nov 2021 19:04:00 +0100 Subject: [PATCH 0166/1608] fix: wording (#725) --- .../operator/processing/event/EventProcessor.java | 2 +- .../operator/processing/event/EventSourceManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 faaf21910b..b3a2fd2896 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 @@ -102,7 +102,7 @@ public void handleEvent(Event event) { try { log.debug("Received event: {}", event); if (!this.running) { - log.debug("Skipping event: {} because the event handler is shutting down", event); + log.debug("Skipping event: {} because the event handler is not started", event); return; } final var resourceID = event.getRelatedCustomResourceID(); 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 64333f8981..9cc7022e68 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 @@ -60,7 +60,7 @@ public void start() throws OperatorException { try { eventSource.start(); } catch (Exception e) { - log.warn("Error closing {} -> {}", eventSource, e); + log.warn("Error starting {} -> {}", eventSource, e); } } } finally { From aab7d05dcdffee0b385e7c709ce7a1a9a8e5e1ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Dec 2021 08:58:12 +0100 Subject: [PATCH 0167/1608] chore(deps): bump formatter-maven-plugin from 2.17.0 to 2.17.1 (#730) Bumps [formatter-maven-plugin](https://github.com/revelc/formatter-maven-plugin) from 2.17.0 to 2.17.1. - [Release notes](https://github.com/revelc/formatter-maven-plugin/releases) - [Changelog](https://github.com/revelc/formatter-maven-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/revelc/formatter-maven-plugin/compare/formatter-maven-plugin-2.17.0...formatter-maven-plugin-2.17.1) --- updated-dependencies: - dependency-name: net.revelc.code.formatter:formatter-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4478c0d50e..a787ef5bc7 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 2.8.2 2.5.2 5.0.0 - 2.17.0 + 2.17.1 1.0 1.6.2 From a01b7f8cb373ce4765197be10c7b31146e4dda2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 6 Dec 2021 11:59:46 +0100 Subject: [PATCH 0168/1608] fix: e2e test tomcat TLS issue (#734) --- .../io/javaoperatorsdk/operator/sample/deployment.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml index 2f6f373c6c..55d1a6be7d 100644 --- a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml @@ -29,11 +29,11 @@ spec: - mountPath: /usr/local/tomcat/webapps name: webapps-volume - name: war-downloader - image: busybox:1.28 + image: busybox:1.34.1 command: ['tail', '-f', '/dev/null'] volumeMounts: - name: webapps-volume mountPath: /data volumes: - name: webapps-volume - emptydir: {} + emptyDir: {} From 9b9de3e3181e90aaaacaaff185c24eaef12e20a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 6 Dec 2021 14:07:36 +0100 Subject: [PATCH 0169/1608] refactor: tomcat example improvements (#735) --- .../operator/sample/TomcatReconciler.java | 40 ++----------------- .../operator/sample/WebappReconciler.java | 11 +++-- .../operator/sample/service.yaml | 5 +++ 3 files changed, 16 insertions(+), 40 deletions(-) diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index ed6faa2e1c..ce3803534a 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -13,23 +13,21 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.RollableScalableResource; -import io.fabric8.kubernetes.client.dsl.ServiceResource; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; -import io.javaoperatorsdk.operator.api.*; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.source.InformerEventSource; +import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; import static java.util.Collections.EMPTY_SET; /** * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also * creates a Service over which the Pods can be accessed. */ -@ControllerConfiguration +@ControllerConfiguration(finalizerName = NO_FINALIZER) public class TomcatReconciler implements Reconciler, EventSourceInitializer { private final Logger log = LoggerFactory.getLogger(getClass()); @@ -81,13 +79,6 @@ public UpdateControl reconcile(Tomcat tomcat, Context context) { return UpdateControl.noUpdate(); } - @Override - public DeleteControl cleanup(Tomcat tomcat, Context context) { - deleteDeployment(tomcat); - deleteService(tomcat); - return DeleteControl.defaultDelete(); - } - private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { DeploymentStatus deploymentStatus = Objects.requireNonNullElse(deployment.getStatus(), new DeploymentStatus()); @@ -156,41 +147,18 @@ private void createOrUpdateDeployment(Tomcat tomcat) { } } - private void deleteDeployment(Tomcat tomcat) { - log.info("Deleting Deployment {}", tomcat.getMetadata().getName()); - RollableScalableResource deployment = - kubernetesClient - .apps() - .deployments() - .inNamespace(tomcat.getMetadata().getNamespace()) - .withName(tomcat.getMetadata().getName()); - if (deployment.get() != null) { - deployment.delete(); - } - } - private void createOrUpdateService(Tomcat tomcat) { Service service = loadYaml(Service.class, "service.yaml"); service.getMetadata().setName(tomcat.getMetadata().getName()); String ns = tomcat.getMetadata().getNamespace(); service.getMetadata().setNamespace(ns); + service.getMetadata().getOwnerReferences().get(0).setName(tomcat.getMetadata().getName()); + service.getMetadata().getOwnerReferences().get(0).setUid(tomcat.getMetadata().getUid()); service.getSpec().getSelector().put("app", tomcat.getMetadata().getName()); log.info("Creating or updating Service {} in {}", service.getMetadata().getName(), ns); kubernetesClient.services().inNamespace(ns).createOrReplace(service); } - private void deleteService(Tomcat tomcat) { - log.info("Deleting Service {}", tomcat.getMetadata().getName()); - ServiceResource service = - kubernetesClient - .services() - .inNamespace(tomcat.getMetadata().getNamespace()) - .withName(tomcat.getMetadata().getName()); - if (service.get() != null) { - service.delete(); - } - } - private T loadYaml(Class clazz, String yaml) { try (InputStream is = getClass().getResourceAsStream(yaml)) { return Serialization.unmarshal(is, clazz); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index f536d87013..115ba9f648 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -17,6 +17,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; +import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -40,9 +41,11 @@ public WebappReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } + private InformerEventSource tomcatEventSource; + @Override public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { - InformerEventSource tomcatEventSource = + tomcatEventSource = new InformerEventSource<>(kubernetesClient, Tomcat.class, t -> { // To create an event to a related WebApp resource and trigger the reconciliation // we need to find which WebApp this Tomcat custom resource is related to. @@ -67,9 +70,9 @@ public UpdateControl reconcile(Webapp webapp, Context context) { return UpdateControl.noUpdate(); } - var tomcatClient = kubernetesClient.customResources(Tomcat.class); - Tomcat tomcat = tomcatClient.inNamespace(webapp.getMetadata().getNamespace()) - .withName(webapp.getSpec().getTomcat()).get(); + Tomcat tomcat = tomcatEventSource.getStore() + .getByKey(Cache.namespaceKeyFunc(webapp.getMetadata().getNamespace(), + webapp.getSpec().getTomcat())); if (tomcat == null) { throw new IllegalStateException("Cannot find Tomcat " + webapp.getSpec().getTomcat() + " for Webapp " + webapp.getMetadata().getName() + " in namespace " diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml index ab198643ed..a807d277a7 100644 --- a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml @@ -2,6 +2,11 @@ apiVersion: v1 kind: Service metadata: name: "" + ownerReferences: # used for finding which Tomcat does this Deployment belong to + - apiVersion: apps/v1 + kind: Tomcat + name: "" + uid: "" spec: selector: app: "" From 3f68140c584a7fb562e161402108caecda22603e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 6 Dec 2021 17:29:19 +0100 Subject: [PATCH 0170/1608] fix: issue with event source start ordering (#736) --- .../processing/event/EventSourceManager.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 9cc7022e68..50f2af8641 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,9 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; @@ -25,7 +22,10 @@ public class EventSourceManager private static final Logger log = LoggerFactory.getLogger(EventSourceManager.class); private final ReentrantLock lock = new ReentrantLock(); - private final Set eventSources = Collections.synchronizedSet(new HashSet<>()); + // This needs to be a list since the event source must be started in a deterministic order. The + // controllerResourceEventSource must be always the first to have informers available for other + // informers to access the main controller cache. + private final List eventSources = Collections.synchronizedList(new ArrayList<>()); private final EventProcessor eventProcessor; private TimerEventSource retryAndRescheduleTimerEventSource; private ControllerResourceEventSource controllerResourceEventSource; @@ -120,7 +120,7 @@ public void cleanupForCustomResource(ResourceID customResourceUid) { @Override public Set getRegisteredEventSources() { - return Collections.unmodifiableSet(eventSources); + return new HashSet<>(eventSources); } @Override From 3c75b78e78b049adb6fac857a4155b3024fb79cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Dec 2021 13:12:24 +0100 Subject: [PATCH 0171/1608] feature: move mysql operator to this repo (#737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Sándor adam.sandor@container-solutions.com --- sample-operators/mysql-schema/README.md | 76 ++++++++ .../mysql-schema/k8s/mysql-deployment.yaml | 25 +++ .../mysql-schema/k8s/mysql-service.yaml | 10 ++ .../mysql-schema/k8s/operator.yaml | 101 +++++++++++ sample-operators/mysql-schema/k8s/schema.yaml | 6 + sample-operators/mysql-schema/pom.xml | 96 ++++++++++ .../operator/sample/MySQLDbConfig.java | 46 +++++ .../operator/sample/MySQLSchema.java | 11 ++ .../operator/sample/MySQLSchemaOperator.java | 34 ++++ .../sample/MySQLSchemaReconciler.java | 168 ++++++++++++++++++ .../operator/sample/SchemaSpec.java | 14 ++ .../operator/sample/SchemaStatus.java | 44 +++++ .../src/main/resources/log4j2.xml | 13 ++ .../sample/MySQLSchemaOperatorE2E.java | 112 ++++++++++++ sample-operators/pom.xml | 1 + sample-operators/tomcat-operator/pom.xml | 10 +- .../operator/sample/TomcatOperatorE2E.java | 2 +- 17 files changed, 765 insertions(+), 4 deletions(-) create mode 100644 sample-operators/mysql-schema/README.md create mode 100644 sample-operators/mysql-schema/k8s/mysql-deployment.yaml create mode 100644 sample-operators/mysql-schema/k8s/mysql-service.yaml create mode 100644 sample-operators/mysql-schema/k8s/operator.yaml create mode 100644 sample-operators/mysql-schema/k8s/schema.yaml create mode 100644 sample-operators/mysql-schema/pom.xml create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLDbConfig.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchema.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaSpec.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java create mode 100644 sample-operators/mysql-schema/src/main/resources/log4j2.xml create mode 100644 sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java diff --git a/sample-operators/mysql-schema/README.md b/sample-operators/mysql-schema/README.md new file mode 100644 index 0000000000..366dcf3e42 --- /dev/null +++ b/sample-operators/mysql-schema/README.md @@ -0,0 +1,76 @@ +# MySQL Schema Operator + +This example shows how an operator can control resources outside of the Kubernetes cluster. In this case it will be +managing MySQL schemas in an existing database server. This is a common scenario in many organizations where developers +need to create schemas for different applications and environments, but the database server itself is managed by a +different team. Using this operator a dev team can create a CR in their namespace and have a schema provisioned automatically. +Access to the MySQL server is configured in the configuration of the operator, so admin access is restricted. + +This is an example input: +```yaml +apiVersion: "mysql.sample.javaoperatorsdk/v1" +kind: MySQLSchema +metadata: + name: mydb +spec: + encoding: utf8 +``` + +Creating this custom resource will prompt the operator to create a schema named `mydb` in the MySQL server and update +the resource status with its URL. Once the resource is deleted, the operator will delete the schema. Obviously don't +use it as is with real databases. + +### Try + +To try how the operator works you will need the following: +* JDK installed (minimum version 11, tested with 11 and 15) +* Maven installed (tested with 3.6.3) +* A working Kubernetes cluster (tested with v1.15.9-gke.24) +* kubectl installed (tested with v1.15.5) +* Docker installed (tested with 19.03.8) +* Container image registry + +How to configure all the above depends heavily on where your Kubernetes cluster is hosted. +If you use [minikube](https://minikube.sigs.k8s.io/docs/) you will need to configure kubectl and docker differently +than if you'd use [GKE](https://cloud.google.com/kubernetes-engine/). You will have to read the documentation of your +Kubernetes provider to figure this out. + +Once you have the basics you can build and deploy the operator. + +### Build & Deploy + +1. We will be building the Docker image from the source code using Maven, so we have to configure the Docker registry +where the image should be pushed. Do this in mysql-schema/pom.xml. In the example below I'm setting it to +the [Container Registry](https://cloud.google.com/container-registry/) in Google Cloud Europe. + +```xml + + eu.gcr.io/my-gcp-project/mysql-operator + +``` + +1. The following Maven command will build the JAR file, package it as a Docker image and push it to the registry. + + `mvn jib:dockerBuild` + +1. Deploy the test MySQL on your cluster if you want to use it. Note that if you have an already running MySQL server +you want to use, you can skip this step, but you will have to configure the operator to use that server. + + `kubectl apply -f k8s/mysql-db.yaml` +1. Deploy the CRD: + + `kubectl apply -f k8s/crd.yaml` + +1. Make a copy of `k8s/operator.yaml` and replace ${DOCKER_REGISTRY} and ${OPERATOR_VERSION} to the +right values. You will want to set `OPERATOR_VERSION` to the one used for building the Docker image. `DOCKER_REGISTRY` should +be the same as you set the docker-registry property in your `pom.xml`. +If you look at the environment variables you will notice this is where the access to the MySQL server is configured. +The default values assume the server is running in another Kubernetes namespace (called `mysql`), uses the `root` user +with a not very secure password. In case you want to use a different MySQL server, this is where you configure it. + +1. Run `kubectl apply -f copy-of-operator.yaml` to deploy the operator. You can wait for the deployment to succeed using +this command: `kubectl rollout status deployment mysql-schema-operator -w`. `-w` will cause kubectl to continuously monitor +the deployment until you stop it. + +1. Now you are ready to create some databases! To create a database schema called `mydb` just apply the `k8s/schema.yaml` +file with kubectl: `kubectl apply -f k8s/schema.yaml`. You can modify the database name in the file to create more schemas. diff --git a/sample-operators/mysql-schema/k8s/mysql-deployment.yaml b/sample-operators/mysql-schema/k8s/mysql-deployment.yaml new file mode 100644 index 0000000000..9dfe210212 --- /dev/null +++ b/sample-operators/mysql-schema/k8s/mysql-deployment.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql +spec: + selector: + matchLabels: + app: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: mysql + spec: + containers: + - image: mysql:5.6 + name: mysql + env: + # Use secret in real usage + - name: MYSQL_ROOT_PASSWORD + value: password + ports: + - containerPort: 3306 + name: mysql \ No newline at end of file diff --git a/sample-operators/mysql-schema/k8s/mysql-service.yaml b/sample-operators/mysql-schema/k8s/mysql-service.yaml new file mode 100644 index 0000000000..3b4188373d --- /dev/null +++ b/sample-operators/mysql-schema/k8s/mysql-service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql +spec: + ports: + - port: 3306 + selector: + app: mysql + type: LoadBalancer \ No newline at end of file diff --git a/sample-operators/mysql-schema/k8s/operator.yaml b/sample-operators/mysql-schema/k8s/operator.yaml new file mode 100644 index 0000000000..f3e667f3ee --- /dev/null +++ b/sample-operators/mysql-schema/k8s/operator.yaml @@ -0,0 +1,101 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: mysql-schema-operator +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql-schema-operator + namespace: mysql-schema-operator +spec: + selector: + matchLabels: + app: mysql-schema-operator + replicas: 1 # we always run a single replica of the operator to avoid duplicate handling of events + strategy: + type: Recreate # during an upgrade the operator will shut down before the new version comes up to prevent two instances running at the same time + template: + metadata: + labels: + app: mysql-schema-operator + spec: + serviceAccountName: mysql-schema-operator # specify the ServiceAccount under which's RBAC persmissions the operator will be executed under + containers: + - name: operator + image: ${DOCKER_REGISTRY}/mysql-schema-operator:${OPERATOR_VERSION} + imagePullPolicy: Always + ports: + - containerPort: 80 + env: + - name: MYSQL_HOST + value: mysql.mysql # assuming the MySQL server runs in a namespace called "mysql" on Kubernetes + - name: MYSQL_USER + value: root + - name: MYSQL_PASSWORD + value: password # sample-level security + readinessProbe: + httpGet: + path: /health # when this returns 200 the operator is considered up and running + port: 8080 + initialDelaySeconds: 1 + timeoutSeconds: 1 + livenessProbe: + httpGet: + path: /health # when this endpoint doesn't return 200 the operator is considered broken and get's restarted + port: 8080 + initialDelaySeconds: 30 + timeoutSeconds: 1 + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mysql-schema-operator + namespace: mysql-schema-operator + +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: mysql-schema-operator +rules: +- apiGroups: + - mysql.sample.javaoperatorsdk + resources: + - schemas + verbs: + - "*" +- apiGroups: + - mysql.sample.javaoperatorsdk + resources: + - schemas/status + verbs: + - "*" +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - "get" + - "list" +- apiGroups: + - "" + resources: + - secrets + verbs: + - "*" + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: mysql-schema-operator +subjects: +- kind: ServiceAccount + name: mysql-schema-operator + namespace: mysql-schema-operator +roleRef: + kind: ClusterRole + name: mysql-schema-operator + apiGroup: "" diff --git a/sample-operators/mysql-schema/k8s/schema.yaml b/sample-operators/mysql-schema/k8s/schema.yaml new file mode 100644 index 0000000000..054c9a1695 --- /dev/null +++ b/sample-operators/mysql-schema/k8s/schema.yaml @@ -0,0 +1,6 @@ +apiVersion: "mysql.sample.javaoperatorsdk/v1" +kind: MySQLSchema +metadata: + name: mydb +spec: + encoding: utf8 \ No newline at end of file diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml new file mode 100644 index 0000000000..393b8dab36 --- /dev/null +++ b/sample-operators/mysql-schema/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + + io.javaoperatorsdk + sample-operators + 2.0.0-SNAPSHOT + + + sample-mysql-schema-operator + Operator SDK - Samples - MySQL Schema + Provisions Schemas in a MySQL database + jar + + + 11 + 11 + 3.1.4 + + + + + io.javaoperatorsdk + operator-framework + ${project.version} + + + org.takes + takes + 1.19 + + + mysql + mysql-connector-java + 8.0.26 + + + io.fabric8 + crd-generator-apt + provided + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.13.3 + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.awaitility + awaitility + 4.1.0 + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.13.0 + + + + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + gcr.io/distroless/java:11 + + + mysql-schema-operator + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + + \ No newline at end of file diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLDbConfig.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLDbConfig.java new file mode 100644 index 0000000000..7cc06dd373 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLDbConfig.java @@ -0,0 +1,46 @@ +package io.javaoperatorsdk.operator.sample; + +import org.apache.commons.lang3.ObjectUtils; + +public class MySQLDbConfig { + + private final String host; + private final String port; + private final String user; + private final String password; + + public MySQLDbConfig(String host, String port, String user, String password) { + this.host = host; + this.port = port != null ? port : "3306"; + this.user = user; + this.password = password; + } + + public static MySQLDbConfig loadFromEnvironmentVars() { + if (ObjectUtils.anyNull(System.getenv("MYSQL_HOST"), + System.getenv("MYSQL_USER"), System.getenv("MYSQL_PASSWORD"))) { + throw new IllegalStateException("Mysql server parameters not defined"); + } + return new MySQLDbConfig(System.getenv("MYSQL_HOST"), + System.getenv("MYSQL_PORT"), + System.getenv("MYSQL_USER"), + System.getenv("MYSQL_PASSWORD")); + } + + public String getHost() { + return host; + } + + public String getPort() { + return port; + } + + public String getUser() { + return user; + } + + public String getPassword() { + return password; + } +} + diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchema.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchema.java new file mode 100644 index 0000000000..80eb25f8c7 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchema.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.sample; + +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.Version; + +@Group("mysql.sample.javaoperatorsdk") +@Version("v1") +public class MySQLSchema extends CustomResource implements Namespaced { +} 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 new file mode 100644 index 0000000000..e8f25c84bd --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperator.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; + +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.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; + +public class MySQLSchemaOperator { + + private static final Logger log = LoggerFactory.getLogger(MySQLSchemaOperator.class); + + public static void main(String[] args) throws IOException { + log.info("MySQL Schema Operator starting"); + + Config config = new ConfigBuilder().withNamespace(null).build(); + KubernetesClient client = new DefaultKubernetesClient(config); + Operator operator = new Operator(client, DefaultConfigurationService.instance()); + operator.register(new MySQLSchemaReconciler(client, MySQLDbConfig.loadFromEnvironmentVars())); + 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/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java new file mode 100644 index 0000000000..47d44246e3 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -0,0 +1,168 @@ +package io.javaoperatorsdk.operator.sample; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Base64; + +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.*; + +import static java.lang.String.format; + +@ControllerConfiguration +public class MySQLSchemaReconciler implements Reconciler { + static final String USERNAME_FORMAT = "%s-user"; + static final String SECRET_FORMAT = "%s-secret"; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final KubernetesClient kubernetesClient; + private final MySQLDbConfig mysqlDbConfig; + + public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig mysqlDbConfig) { + this.kubernetesClient = kubernetesClient; + this.mysqlDbConfig = mysqlDbConfig; + } + + @Override + public UpdateControl reconcile(MySQLSchema schema, + Context context) { + try (Connection connection = getConnection()) { + if (!schemaExists(connection, schema.getMetadata().getName())) { + try (Statement statement = connection.createStatement()) { + statement.execute( + format( + "CREATE SCHEMA `%1$s` DEFAULT CHARACTER SET %2$s", + schema.getMetadata().getName(), schema.getSpec().getEncoding())); + } + + String password = RandomStringUtils.randomAlphanumeric(16); + String userName = String.format(USERNAME_FORMAT, schema.getMetadata().getName()); + String secretName = String.format(SECRET_FORMAT, schema.getMetadata().getName()); + try (Statement statement = connection.createStatement()) { + statement.execute(format("CREATE USER '%1$s' IDENTIFIED BY '%2$s'", userName, password)); + } + try (Statement statement = connection.createStatement()) { + statement.execute( + format("GRANT ALL ON `%1$s`.* TO '%2$s'", schema.getMetadata().getName(), userName)); + } + Secret credentialsSecret = + new SecretBuilder() + .withNewMetadata() + .withName(secretName) + .endMetadata() + .addToData( + "MYSQL_USERNAME", Base64.getEncoder().encodeToString(userName.getBytes())) + .addToData( + "MYSQL_PASSWORD", Base64.getEncoder().encodeToString(password.getBytes())) + .build(); + this.kubernetesClient + .secrets() + .inNamespace(schema.getMetadata().getNamespace()) + .create(credentialsSecret); + + SchemaStatus status = new SchemaStatus(); + status.setUrl( + format( + "jdbc:mysql://%1$s/%2$s", + System.getenv("MYSQL_HOST"), schema.getMetadata().getName())); + status.setUserName(userName); + status.setSecretName(secretName); + status.setStatus("CREATED"); + schema.setStatus(status); + log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); + + return UpdateControl.updateStatus(schema); + } + return UpdateControl.noUpdate(); + } catch (SQLException e) { + log.error("Error while creating Schema", e); + + SchemaStatus status = new SchemaStatus(); + status.setUrl(null); + status.setUserName(null); + status.setSecretName(null); + status.setStatus("ERROR: " + e.getMessage()); + schema.setStatus(status); + + return UpdateControl.updateStatus(schema); + } + } + + @Override + public DeleteControl cleanup(MySQLSchema schema, Context context) { + log.info("Execution deleteResource for: {}", schema.getMetadata().getName()); + + try (Connection connection = getConnection()) { + if (schemaExists(connection, schema.getMetadata().getName())) { + try (Statement statement = connection.createStatement()) { + statement.execute(format("DROP DATABASE `%1$s`", schema.getMetadata().getName())); + } + log.info("Deleted Schema '{}'", schema.getMetadata().getName()); + + if (schema.getStatus() != null) { + if (userExists(connection, schema.getStatus().getUserName())) { + try (Statement statement = connection.createStatement()) { + statement.execute(format("DROP USER '%1$s'", schema.getStatus().getUserName())); + } + log.info("Deleted User '{}'", schema.getStatus().getUserName()); + } + } + + this.kubernetesClient + .secrets() + .inNamespace(schema.getMetadata().getNamespace()) + .withName(schema.getStatus().getSecretName()) + .delete(); + } else { + log.info( + "Delete event ignored for schema '{}', real schema doesn't exist", + schema.getMetadata().getName()); + } + return DeleteControl.defaultDelete(); + } catch (SQLException e) { + log.error("Error while trying to delete Schema", e); + return DeleteControl.noFinalizerRemoval(); + } + } + + private Connection getConnection() throws SQLException { + String connectionString = + format("jdbc:mysql://%1$s:%2$s", mysqlDbConfig.getHost(), mysqlDbConfig.getPort()); + + log.info("Connecting to '{}' with user '{}'", connectionString, mysqlDbConfig.getUser()); + return DriverManager.getConnection(connectionString, mysqlDbConfig.getUser(), + mysqlDbConfig.getPassword()); + } + + private boolean schemaExists(Connection connection, String schemaName) throws SQLException { + try (PreparedStatement ps = + connection.prepareStatement( + "SELECT schema_name FROM information_schema.schemata WHERE schema_name = ?")) { + ps.setString(1, schemaName); + try (ResultSet resultSet = ps.executeQuery()) { + return resultSet.next(); + } + } + } + + private boolean userExists(Connection connection, String userName) throws SQLException { + try (PreparedStatement ps = + connection.prepareStatement("SELECT User FROM mysql.user WHERE User = ?")) { + ps.setString(1, userName); + try (ResultSet resultSet = ps.executeQuery()) { + return resultSet.first(); + } + } + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaSpec.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaSpec.java new file mode 100644 index 0000000000..19101c328a --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample; + +public class SchemaSpec { + + private String encoding; + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java new file mode 100644 index 0000000000..168cd8db15 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java @@ -0,0 +1,44 @@ +package io.javaoperatorsdk.operator.sample; + +public class SchemaStatus { + + private String url; + + private String status; + + private String userName; + + private String secretName; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getSecretName() { + return secretName; + } + + public void setSecretName(String secretName) { + this.secretName = secretName; + } +} diff --git a/sample-operators/mysql-schema/src/main/resources/log4j2.xml b/sample-operators/mysql-schema/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..5ab4735126 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000000..75c5412e9c --- /dev/null +++ b/sample-operators/mysql-schema/src/test/java/io/javaoperatorsdk/operator/sample/MySQLSchemaOperatorE2E.java @@ -0,0 +1,112 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.*; +import io.fabric8.kubernetes.client.*; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; + +@Disabled +public class MySQLSchemaOperatorE2E { + + final static String TEST_NS = "mysql-schema-test"; + + final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); + + @Test + public void test() throws IOException { + Config config = new ConfigBuilder().withNamespace(null).build(); + KubernetesClient client = new DefaultKubernetesClient(config); + + // Use this if you want to run the test without deploying the Operator to Kubernetes + if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) { + Operator operator = new Operator(client, DefaultConfigurationService.instance()); + MySQLDbConfig dbConfig = new MySQLDbConfig("mysql", null, "root", "password"); + operator.register(new MySQLSchemaReconciler(client, dbConfig)); + operator.start(); + } + + MySQLSchema testSchema = new MySQLSchema(); + testSchema.setMetadata(new ObjectMetaBuilder() + .withName("mydb1") + .withNamespace(TEST_NS) + .build()); + testSchema.setSpec(new SchemaSpec()); + testSchema.getSpec().setEncoding("utf8"); + + Namespace testNs = new NamespaceBuilder().withMetadata( + new ObjectMetaBuilder().withName(TEST_NS).build()).build(); + + if (testNs != null) { + // We perform a pre-run cleanup instead of a post-run cleanup. This is to help with debugging + // test results when running against a persistent cluster. The test namespace would stay + // after the test run so we can check what's there, but it would be cleaned up during the next + // test run. + log.info("Cleanup: deleting test namespace {}", TEST_NS); + client.namespaces().delete(testNs); + await().atMost(5, MINUTES) + .until(() -> client.namespaces().withName(TEST_NS).get() == null); + } + + log.info("Creating test namespace {}", TEST_NS); + client.namespaces().create(testNs); + + log.info("Deploying MySQL server"); + deployMySQLServer(client); + + log.info("Creating test MySQLSchema object: {}", testSchema); + // var mysqlSchemaClient = client.customResources(MySQLSchema.class); + // mysqlSchemaClient.inNamespace(TEST_NS).createOrReplace(testSchema); + client.resource(testSchema).createOrReplace(); + + log.info("Waiting 5 minutes for expected resources to be created and updated"); + await().atMost(5, MINUTES).untilAsserted(() -> { + MySQLSchema updatedSchema = client.resources(MySQLSchema.class).inNamespace(TEST_NS) + .withName(testSchema.getMetadata().getName()).get(); + assertThat(updatedSchema.getStatus(), is(notNullValue())); + assertThat(updatedSchema.getStatus().getStatus(), equalTo("CREATED")); + assertThat(updatedSchema.getStatus().getSecretName(), is(notNullValue())); + assertThat(updatedSchema.getStatus().getUserName(), is(notNullValue())); + }); + } + + private void deployMySQLServer(KubernetesClient client) throws IOException { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + Deployment deployment = + mapper.readValue(new File("k8s/mysql-deployment.yaml"), Deployment.class); + deployment.getMetadata().setNamespace(TEST_NS); + Service service = mapper.readValue(new File("k8s/mysql-service.yaml"), Service.class); + service.getMetadata().setNamespace(TEST_NS); + client.resource(deployment).createOrReplace(); + client.resource(service).createOrReplace(); + + log.info("Waiting for MySQL server to start"); + await().atMost(5, MINUTES).until(() -> { + Deployment mysqlDeployment = client.apps().deployments().inNamespace(TEST_NS) + .withName(deployment.getMetadata().getName()).get(); + return mysqlDeployment.getStatus().getReadyReplicas() != null + && mysqlDeployment.getStatus().getReadyReplicas() == 1; + }); + } + +} diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 5d9a497cec..cb47a55049 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -21,5 +21,6 @@ tomcat-operator webpage + mysql-schema \ No newline at end of file diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index ae8605f305..52fdc81cc6 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -43,9 +43,13 @@ 1.19 - junit - junit - 4.13.2 + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine test diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index f3cd473294..e803b70aba 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator.sample; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 53cace1f1ca945ea63a17dee88f1b1877a5e290d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Dec 2021 08:18:21 +0100 Subject: [PATCH 0172/1608] chore(deps): bump mysql-connector-java from 8.0.26 to 8.0.27 (#740) --- sample-operators/mysql-schema/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 393b8dab36..531ac1a943 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -35,7 +35,7 @@ mysql mysql-connector-java - 8.0.26 + 8.0.27 io.fabric8 From 70f489f6abee08ac0613a89a18c9fa5daa324907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Dec 2021 08:18:44 +0100 Subject: [PATCH 0173/1608] chore(deps): bump micrometer-core from 1.8.0 to 1.8.1 (#741) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a787ef5bc7..f9c4b8db49 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ 3.21.0 4.1.1 2.6.1 - 1.8.0 + 1.8.1 2.11 3.8.1 From 02e9171f7626f6d4f817c43b3abe733fbdc20cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 8 Dec 2021 12:44:42 +0100 Subject: [PATCH 0174/1608] Error status handler improvements (#742) --- docs/documentation/features.md | 12 +++--- .../api/reconciler/ErrorStatusHandler.java | 5 ++- .../event/ReconciliationDispatcher.java | 40 +++++++++--------- .../event/ReconciliationDispatcherTest.java | 35 +++++++++++++--- .../operator/ErrorStatusHandlerIT.java | 14 ++++--- .../io/javaoperatorsdk/operator/RetryIT.java | 13 +++--- .../operator/RetryMaxAttemptIT.java | 42 +++++++++++++++++++ ...StatusHandlerTestCustomResourceStatus.java | 16 ++++--- .../ErrorStatusHandlerTestReconciler.java | 9 ++-- .../retry/RetryTestCustomReconciler.java | 10 +++-- .../operator/sample/WebPageReconciler.java | 6 ++- 11 files changed, 146 insertions(+), 56 deletions(-) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 194effb36e..b710eeb3e3 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -166,19 +166,21 @@ A successful execution resets the retry. ### Setting Error Status After Last Retry Attempt -In order to facilitate error reporting in case a last retry attempt fails, Reconciler can implement the following +In order to facilitate error reporting Reconciler can implement the following [interface](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java): ```java -public interface ErrorStatusHandler> { +public interface ErrorStatusHandler { - T updateErrorStatus(T resource, RuntimeException e); + Optional updateErrorStatus(T resource, RetryInfo retryInfo, RuntimeException e); } ``` -The `updateErrorStatus` resource is called when it's the last retry attempt according the retry configuration and the -reconciler execution still resulted in a runtime exception. +The `updateErrorStatus` method is called in case an exception is thrown from the reconciler. It is also called when +there is no retry configured, just after the reconciler execution. +In the first call the `RetryInfo.getAttemptCount()` is always zero, since it is not a result of a retry +(regardless if retry is configured or not). The result of the method call is used to make a status update on the custom resource. This is always a sub-resource update request, so no update on custom resource itself (like spec of metadata) happens. Note that this update request diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java index 2d5592053b..60e9e671b2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.HasMetadata; public interface ErrorStatusHandler { @@ -19,9 +21,10 @@ public interface ErrorStatusHandler { * should not be updates on custom resource after it is marked for deletion. * * @param resource to update the status on + * @param retryInfo * @param e exception thrown from the reconciler * @return the updated resource */ - T updateErrorStatus(T resource, RuntimeException e); + Optional updateErrorStatus(T resource, RetryInfo retryInfo, RuntimeException e); } 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 01f6de14ac..f29865d338 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 @@ -11,13 +11,7 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.BaseControl; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.Controller; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; @@ -114,7 +108,7 @@ private PostExecutionControl handleReconcile( cloneResourceForErrorStatusHandlerIfNeeded(originalResource, context); return reconcileExecution(executionScope, resourceForExecution, originalResource, context); } catch (RuntimeException e) { - handleLastAttemptErrorStatusHandler(originalResource, context, e); + handleErrorStatusHandler(originalResource, context, e); throw e; } } @@ -128,7 +122,7 @@ private PostExecutionControl handleReconcile( * place for status update. */ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { - if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context) || + if (isErrorStatusHandlerPresent() || shouldUpdateObservedGenerationAutomatically(resource)) { return configuration().getConfigurationService().getResourceCloner().clone(resource); } else { @@ -164,26 +158,32 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { return createPostExecutionControl(updatedCustomResource, updateControl); } - private void handleLastAttemptErrorStatusHandler(R resource, Context context, + private void handleErrorStatusHandler(R resource, Context context, RuntimeException e) { - if (isLastAttemptOfRetryAndErrorStatusHandlerPresent(context)) { + if (isErrorStatusHandlerPresent()) { try { + var retryInfo = context.getRetryInfo().orElse(new RetryInfo() { + @Override + public int getAttemptCount() { + return 0; + } + + @Override + public boolean isLastAttempt() { + return controller.getConfiguration().getRetryConfiguration() == null; + } + }); var updatedResource = ((ErrorStatusHandler) controller.getReconciler()) - .updateErrorStatus(resource, e); - customResourceFacade.updateStatus(updatedResource); + .updateErrorStatus(resource, retryInfo, e); + updatedResource.ifPresent(customResourceFacade::updateStatus); } catch (RuntimeException ex) { log.error("Error during error status handling.", ex); } } } - private boolean isLastAttemptOfRetryAndErrorStatusHandlerPresent(Context context) { - if (context.getRetryInfo().isPresent()) { - return context.getRetryInfo().get().isLastAttempt() - && controller.getReconciler() instanceof ErrorStatusHandler; - } else { - return false; - } + private boolean isErrorStatusHandlerPresent() { + return controller.getReconciler() instanceof ErrorStatusHandler; } private R updateStatusGenerationAware(R resource) { 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 db4736697a..dd9d5e6fda 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 @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; import java.util.ArrayList; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,6 +12,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.monitoring.Metrics; @@ -56,9 +58,12 @@ private ReconciliationDispatcher init(R customResourc when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configService.getMetrics()).thenReturn(Metrics.NOOP); when(configuration.getConfigurationService()).thenReturn(configService); - when(configService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); - when(reconciler.reconcile(eq(customResource), any())) - .thenReturn(UpdateControl.updateResource(customResource)); + when(configService.getResourceCloner()).thenReturn(new Cloner() { + @Override + public R clone(R object) { + return object; + } + }); when(reconciler.cleanup(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); when(customResourceFacade.replaceWithLock(any())).thenReturn(null); @@ -351,9 +356,9 @@ void callErrorStatusHandlerIfImplemented() { when(reconciler.reconcile(any(), any())) .thenThrow(new IllegalStateException("Error Status Test")); - when(((ErrorStatusHandler) reconciler).updateErrorStatus(any(), any())).then(a -> { + when(((ErrorStatusHandler) reconciler).updateErrorStatus(any(), any(), any())).then(a -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); - return testCustomResource; + return Optional.of(testCustomResource); }); reconciliationDispatcher.handleExecution( @@ -373,7 +378,25 @@ public boolean isLastAttempt() { verify(customResourceFacade, times(1)).updateStatus(testCustomResource); verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), - any()); + any(), any()); + } + + @Test + void callErrorStatusHandlerEvenOnFirstError() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + + when(reconciler.reconcile(any(), any())) + .thenThrow(new IllegalStateException("Error Status Test")); + when(((ErrorStatusHandler) reconciler).updateErrorStatus(any(), any(), any())).then(a -> { + testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); + return Optional.of(testCustomResource); + }); + reconciliationDispatcher.handleExecution( + new ExecutionScope( + testCustomResource, null)); + verify(customResourceFacade, times(1)).updateStatus(testCustomResource); + verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), + any(), any()); } private ObservedGenCustomResource createObservedGenCustomResource() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java index 01908757bc..4a1b3293e7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java @@ -17,13 +17,15 @@ public class ErrorStatusHandlerIT { + public static final int MAX_RETRY_ATTEMPTS = 3; ErrorStatusHandlerTestReconciler reconciler = new ErrorStatusHandlerTestReconciler(); @RegisterExtension OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(reconciler, new GenericRetry().setMaxAttempts(3).withLinearRetry()) + .withReconciler(reconciler, + new GenericRetry().setMaxAttempts(MAX_RETRY_ATTEMPTS).withLinearRetry()) .build(); @Test @@ -32,16 +34,18 @@ public void testErrorMessageSetEventually() { operator.create(ErrorStatusHandlerTestCustomResource.class, createCustomResource()); await() - .atMost(15, TimeUnit.SECONDS) - .pollInterval(1L, TimeUnit.SECONDS) + .atMost(10, TimeUnit.SECONDS) + .pollInterval(250, TimeUnit.MICROSECONDS) .untilAsserted( () -> { ErrorStatusHandlerTestCustomResource res = operator.get(ErrorStatusHandlerTestCustomResource.class, resource.getMetadata().getName()); assertThat(res.getStatus()).isNotNull(); - assertThat(res.getStatus().getMessage()) - .isEqualTo(ErrorStatusHandlerTestReconciler.ERROR_STATUS_MESSAGE); + for (int i = 0; i < MAX_RETRY_ATTEMPTS + 1; i++) { + assertThat(res.getStatus().getMessages()) + .contains(ErrorStatusHandlerTestReconciler.ERROR_STATUS_MESSAGE + i); + } }); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index cb7109f8cf..4ab63c7513 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -20,15 +20,18 @@ public class RetryIT { public static final int RETRY_INTERVAL = 150; + public static final int MAX_RETRY_ATTEMPTS = 5; + + public static final int NUMBER_FAILED_EXECUTIONS = 3; @RegisterExtension OperatorExtension operator = OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler( - new RetryTestCustomReconciler(), + new RetryTestCustomReconciler(NUMBER_FAILED_EXECUTIONS), new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry() - .setMaxAttempts(5)) + .setMaxAttempts(MAX_RETRY_ATTEMPTS)) .build(); @@ -40,7 +43,7 @@ public void retryFailedExecution() { await("cr status updated") .pollDelay( - RETRY_INTERVAL * (RetryTestCustomReconciler.NUMBER_FAILED_EXECUTIONS + 2), + RETRY_INTERVAL * (NUMBER_FAILED_EXECUTIONS + 2), TimeUnit.MILLISECONDS) .pollInterval( RETRY_INTERVAL, @@ -49,7 +52,7 @@ public void retryFailedExecution() { .untilAsserted(() -> { assertThat( TestUtils.getNumberOfExecutions(operator)) - .isEqualTo(RetryTestCustomReconciler.NUMBER_FAILED_EXECUTIONS + 1); + .isEqualTo(NUMBER_FAILED_EXECUTIONS + 1); RetryTestCustomResource finalResource = operator.get(RetryTestCustomResource.class, @@ -59,7 +62,7 @@ public void retryFailedExecution() { }); } - public RetryTestCustomResource createTestCustomResource(String id) { + public static RetryTestCustomResource createTestCustomResource(String id) { RetryTestCustomResource resource = new RetryTestCustomResource(); resource.setMetadata( new ObjectMetaBuilder() diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java new file mode 100644 index 0000000000..e855b016fd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java @@ -0,0 +1,42 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; +import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomReconciler; +import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomResource; + +import static io.javaoperatorsdk.operator.RetryIT.createTestCustomResource; +import static org.assertj.core.api.Assertions.assertThat; + +public class RetryMaxAttemptIT { + + public static final int MAX_RETRY_ATTEMPTS = 3; + public static final int RETRY_INTERVAL = 100; + public static final int ALL_EXECUTION_TO_FAIL = 99; + + RetryTestCustomReconciler reconciler = new RetryTestCustomReconciler(ALL_EXECUTION_TO_FAIL); + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(reconciler, + new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry() + .setMaxAttempts(MAX_RETRY_ATTEMPTS)) + .build(); + + + @Test + public void retryFailedExecution() throws InterruptedException { + RetryTestCustomResource resource = createTestCustomResource("max-retry"); + + operator.create(RetryTestCustomResource.class, resource); + + Thread.sleep((MAX_RETRY_ATTEMPTS + 2) * RETRY_INTERVAL); + assertThat(reconciler.getNumberOfExecutions()).isEqualTo(4); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResourceStatus.java index 03fc517ecd..4e54c877a5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResourceStatus.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestCustomResourceStatus.java @@ -1,15 +1,21 @@ package io.javaoperatorsdk.operator.sample.errorstatushandler; +import java.util.ArrayList; +import java.util.List; + public class ErrorStatusHandlerTestCustomResourceStatus { - private String message; + private List messages; - public String getMessage() { - return message; + public List getMessages() { + if (messages == null) { + messages = new ArrayList<>(); + } + return messages; } - public ErrorStatusHandlerTestCustomResourceStatus setMessage(String message) { - this.message = message; + public ErrorStatusHandlerTestCustomResourceStatus setMessages(List messages) { + this.messages = messages; return this; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index 3a922ce092..8f20c44353 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.sample.errorstatushandler; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; @@ -45,11 +46,11 @@ public int getNumberOfExecutions() { } @Override - public ErrorStatusHandlerTestCustomResource updateErrorStatus( - ErrorStatusHandlerTestCustomResource resource, RuntimeException e) { + public Optional updateErrorStatus( + ErrorStatusHandlerTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { log.info("Setting status."); ensureStatusExists(resource); - resource.getStatus().setMessage(ERROR_STATUS_MESSAGE); - return resource; + resource.getStatus().getMessages().add(ERROR_STATUS_MESSAGE + retryInfo.getAttemptCount()); + return Optional.of(resource); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index b2a7ca0e17..21e44631fb 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -17,8 +17,6 @@ public class RetryTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { - public static final int NUMBER_FAILED_EXECUTIONS = 2; - public static final String FINALIZER_NAME = ControllerUtils.getDefaultFinalizerName( CustomResource.getCRDName(RetryTestCustomResource.class)); @@ -26,6 +24,12 @@ public class RetryTestCustomReconciler LoggerFactory.getLogger(RetryTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private int numberOfExecutionFails; + + public RetryTestCustomReconciler(int numberOfExecutionFails) { + this.numberOfExecutionFails = numberOfExecutionFails; + } + @Override public UpdateControl reconcile( RetryTestCustomResource resource, Context context) { @@ -36,7 +40,7 @@ public UpdateControl reconcile( } log.info("Value: " + resource.getSpec().getValue()); - if (numberOfExecutions.get() < NUMBER_FAILED_EXECUTIONS + 1) { + if (numberOfExecutions.get() < numberOfExecutionFails + 1) { throw new RuntimeException("Testing Retry"); } if (context.getRetryInfo().isEmpty() || context.getRetryInfo().get().isLastAttempt()) { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 37caf1503c..ddb353ac05 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -173,8 +174,9 @@ private T loadYaml(Class clazz, String yaml) { } @Override - public WebPage updateErrorStatus(WebPage resource, RuntimeException e) { + public Optional updateErrorStatus(WebPage resource, RetryInfo retryInfo, + RuntimeException e) { resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return resource; + return Optional.of(resource); } } From 5955ecd1dd1ff0886aabee5a441e2664b392ca5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 8 Dec 2021 14:39:30 +0100 Subject: [PATCH 0175/1608] feature: update observed generation on updateResource (#731) --- .../event/ReconciliationDispatcher.java | 3 +++ .../event/ReconciliationDispatcherTest.java | 22 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) 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 f29865d338..314698fd71 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 @@ -151,6 +151,9 @@ private PostExecutionControl reconcileExecution(ExecutionScope executionSc updatedCustomResource = updateStatusGenerationAware(updateControl.getResource()); } else if (updateControl.isUpdateResource()) { updatedCustomResource = updateCustomResource(updateControl.getResource()); + if (shouldUpdateObservedGenerationAutomatically(updatedCustomResource)) { + updatedCustomResource = updateStatusGenerationAware(originalResource); + } } else if (updateControl.isNoUpdate() && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { updatedCustomResource = updateStatusGenerationAware(originalResource); 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 dd9d5e6fda..4de29f596c 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 @@ -66,7 +66,6 @@ public R clone(R object) { }); when(reconciler.cleanup(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); - when(customResourceFacade.replaceWithLock(any())).thenReturn(null); Controller controller = new Controller<>(reconciler, configuration, null); @@ -350,6 +349,27 @@ void updatesObservedGenerationOnNoUpdateUpdateControl() { .isEqualTo(1L); } + @Test + void updateObservedGenerationOnCustomResourceUpdate() { + var observedGenResource = createObservedGenCustomResource(); + + Reconciler reconciler = mock(Reconciler.class); + ControllerConfiguration config = + mock(ControllerConfiguration.class); + CustomResourceFacade facade = mock(CustomResourceFacade.class); + when(config.isGenerationAware()).thenReturn(true); + when(reconciler.reconcile(any(), any())) + .thenReturn(UpdateControl.updateResource(observedGenResource)); + when(facade.replaceWithLock(any())).thenReturn(observedGenResource); + when(facade.updateStatus(observedGenResource)).thenReturn(observedGenResource); + var dispatcher = init(observedGenResource, reconciler, config, facade); + + PostExecutionControl control = dispatcher.handleExecution( + executionScopeWithCREvent(observedGenResource)); + assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) + .isEqualTo(1L); + } + @Test void callErrorStatusHandlerIfImplemented() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); From 806e4ea02421f86f5040a0528e502c9ed5de26c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 10 Dec 2021 10:55:43 +0100 Subject: [PATCH 0176/1608] fix: dependency log4j sec issue (#745) --- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 1 - sample-operators/tomcat-operator/pom.xml | 1 - smoke-test-samples/spring-boot-plain/pom.xml | 5 +++++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f9c4b8db49..fe9a567707 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 5.8.2 5.10.1 1.7.32 - 2.14.1 + 2.15.0 4.1.0 3.12.0 1.0.1 diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 531ac1a943..8693cf6154 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -45,7 +45,6 @@ org.apache.logging.log4j log4j-slf4j-impl - 2.13.3 org.junit.jupiter diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 52fdc81cc6..85edf9bbaf 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -35,7 +35,6 @@ org.apache.logging.log4j log4j-slf4j-impl - 2.13.3 org.takes diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index de64a684e5..ce82ac6b2d 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -45,6 +45,11 @@ + + org.apache.logging.log4j + log4j-api + ${log4j.version} + From e8e05c254061da2480e73858915a19b7d1a5656b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 10 Dec 2021 12:47:45 +0100 Subject: [PATCH 0177/1608] fix: snapshot release for v1 (#747) --- .github/workflows/master-snapshot-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index 69e3e009cb..169ce0d6fe 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: true on: push: - branches: [ main ] + branches: [ main, v1 ] workflow_dispatch: jobs: test: From 29fe326c537cf771eeca68223a862327a6cc4b3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Dec 2021 08:16:20 +0100 Subject: [PATCH 0178/1608] chore(deps): bump manusa/actions-setup-minikube from 2.4.2 to 2.4.3 (#751) Bumps [manusa/actions-setup-minikube](https://github.com/manusa/actions-setup-minikube) from 2.4.2 to 2.4.3. - [Release notes](https://github.com/manusa/actions-setup-minikube/releases) - [Commits](https://github.com/manusa/actions-setup-minikube/compare/v2.4.2...v2.4.3) --- updated-dependencies: - dependency-name: manusa/actions-setup-minikube dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/master-snapshot-release.yml | 2 +- .github/workflows/pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index 169ce0d6fe..abb22c9a65 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -29,7 +29,7 @@ jobs: - name: Run unit tests run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml - name: Set up Minikube - uses: manusa/actions-setup-minikube@v2.4.2 + uses: manusa/actions-setup-minikube@v2.4.3 with: minikube version: 'v1.22.0' kubernetes version: ${{ matrix.kubernetes }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5a9287ce08..e51c329878 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -33,7 +33,7 @@ jobs: - name: Run unit tests run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml - name: Set up Minikube - uses: manusa/actions-setup-minikube@v2.4.2 + uses: manusa/actions-setup-minikube@v2.4.3 with: minikube version: 'v1.24.0' kubernetes version: ${{ matrix.kubernetes }} From b8669c81da827fa1ff082a575119000720223e3e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 13 Dec 2021 10:59:26 +0100 Subject: [PATCH 0179/1608] refactor: more controller -> reconciler renaming (#750) --- .../io/javaoperatorsdk/operator/Operator.java | 36 ++++++++--------- ...trollerUtils.java => ReconcilerUtils.java} | 26 ++++++------ .../config/AbstractConfigurationService.java | 35 ++++++++-------- .../api/config/BaseConfigurationService.java | 6 +-- .../api/config/ConfigurationService.java | 23 +++++------ .../config/ConfigurationServiceOverrider.java | 8 ++-- .../api/config/ControllerConfiguration.java | 6 +-- .../operator/api/monitoring/Metrics.java | 6 +-- ...tilsTest.java => ReconcilerUtilsTest.java} | 4 +- .../operator/junit/OperatorExtension.java | 40 +++++++++---------- .../runtime/AnnotationConfiguration.java | 6 +-- .../runtime/DefaultConfigurationService.java | 4 +- .../DefaultConfigurationServiceTest.java | 8 ++-- .../EventSourceTestCustomReconciler.java | 4 +- .../retry/RetryTestCustomReconciler.java | 4 +- .../sample/simple/TestReconciler.java | 4 +- .../SubResourceTestCustomReconciler.java | 4 +- 17 files changed, 109 insertions(+), 115 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{ControllerUtils.java => ReconcilerUtils.java} (54%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/{ControllerUtilsTest.java => ReconcilerUtilsTest.java} (82%) 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 d860a1eb7e..415e5c3505 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 @@ -107,11 +107,11 @@ public void close() { } /** - * Add a registration requests for the specified controller with this operator. The effective - * registration of the controller is delayed till the operator is started. + * Add a registration requests for the specified reconciler with this operator. The effective + * registration of the reconciler is delayed till the operator is started. * - * @param reconciler the controller to register - * @param the {@code CustomResource} type associated with the controller + * @param reconciler the reconciler to register + * @param the {@code CustomResource} type associated with the reconciler * @throws OperatorException if a problem occurred during the registration process */ public void register(Reconciler reconciler) @@ -121,15 +121,15 @@ public void register(Reconciler reconciler) } /** - * Add a registration requests for the specified controller with this operator, overriding its + * Add a registration requests for the specified reconciler with this operator, overriding its * default configuration by the specified one (usually created via * {@link io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider#override(ControllerConfiguration)}, - * passing it the controller's original configuration. The effective registration of the - * controller is delayed till the operator is started. + * passing it the reconciler's original configuration. The effective registration of the + * reconciler is delayed till the operator is started. * - * @param reconciler part of the controller to register - * @param configuration the configuration with which we want to register the controller - * @param the {@code CustomResource} type associated with the controller + * @param reconciler part of the reconciler to register + * @param configuration the configuration with which we want to register the reconciler + * @param the {@code CustomResource} type associated with the reconciler * @throws OperatorException if a problem occurred during the registration process */ public void register(Reconciler reconciler, @@ -138,10 +138,10 @@ public void register(Reconciler reconciler, if (configuration == null) { throw new OperatorException( - "Cannot register controller with name " + reconciler.getClass().getCanonicalName() + - " controller named " + ControllerUtils.getNameFor(reconciler) + "Cannot register reconciler with name " + reconciler.getClass().getCanonicalName() + + " reconciler named " + ReconcilerUtils.getNameFor(reconciler) + " because its configuration cannot be found.\n" + - " Known controllers are: " + configurationService.getKnownControllerNames()); + " Known reconcilers are: " + configurationService.getKnownReconcilerNames()); } final var controller = new Controller<>(reconciler, configuration, kubernetesClient); @@ -152,7 +152,7 @@ public void register(Reconciler reconciler, : configuration.getEffectiveNamespaces(); log.info( - "Registered Controller: '{}' for CRD: '{}' for namespace(s): {}", + "Registered reconciler: '{}' for resource: '{}' for namespace(s): {}", configuration.getName(), configuration.getResourceClass(), watchedNS); @@ -191,14 +191,14 @@ public synchronized void stop() { public synchronized void add(Controller controller) { final var configuration = controller.getConfiguration(); - final var crdName = configuration.getResourceTypeName(); - final var existing = controllers.get(crdName); + final var resourceTypeName = configuration.getResourceTypeName(); + final var existing = controllers.get(resourceTypeName); if (existing != null) { throw new OperatorException("Cannot register controller '" + configuration.getName() + "': another controller named '" + existing.getConfiguration().getName() - + "' is already registered for CRD '" + crdName + "'"); + + "' is already registered for resource '" + resourceTypeName + "'"); } - this.controllers.put(crdName, controller); + this.controllers.put(resourceTypeName, controller); if (started) { controller.start(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java similarity index 54% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index cd3ca8f916..487c923349 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ControllerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -6,7 +6,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @SuppressWarnings("rawtypes") -public class ControllerUtils { +public class ReconcilerUtils { private static final String FINALIZER_NAME_SUFFIX = "/finalizer"; @@ -14,9 +14,9 @@ public static String getDefaultFinalizerName(String crdName) { return crdName + FINALIZER_NAME_SUFFIX; } - public static String getNameFor(Class controllerClass) { - // if the controller annotation has a name attribute, use it - final var annotation = controllerClass.getAnnotation(ControllerConfiguration.class); + public static String getNameFor(Class reconcilerClass) { + // if the reconciler annotation has a name attribute, use it + final var annotation = reconcilerClass.getAnnotation(ControllerConfiguration.class); if (annotation != null) { final var name = annotation.name(); if (!ControllerConfiguration.EMPTY_STRING.equals(name)) { @@ -24,27 +24,27 @@ public static String getNameFor(Class controllerClass) { } } // otherwise, use the lower-cased full class name - return getDefaultNameFor(controllerClass); + return getDefaultNameFor(reconcilerClass); } - public static String getNameFor(Reconciler controller) { - return getNameFor(controller.getClass()); + public static String getNameFor(Reconciler reconciler) { + return getNameFor(reconciler.getClass()); } - public static String getDefaultNameFor(Reconciler controller) { - return getDefaultNameFor(controller.getClass()); + public static String getDefaultNameFor(Reconciler reconciler) { + return getDefaultNameFor(reconciler.getClass()); } public static String getDefaultNameFor(Class reconcilerClass) { return getDefaultReconcilerName(reconcilerClass.getSimpleName()); } - public static String getDefaultReconcilerName(String rcControllerClassName) { + public static String getDefaultReconcilerName(String reconcilerClassName) { // if the name is fully qualified, extract the simple class name - final var lastDot = rcControllerClassName.lastIndexOf('.'); + final var lastDot = reconcilerClassName.lastIndexOf('.'); if (lastDot > 0) { - rcControllerClassName = rcControllerClassName.substring(lastDot + 1); + reconcilerClassName = reconcilerClassName.substring(lastDot + 1); } - return rcControllerClassName.toLowerCase(Locale.ROOT); + return reconcilerClassName.toLowerCase(Locale.ROOT); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index 0dda134c11..48c6b73f2b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -6,7 +6,7 @@ import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @SuppressWarnings("rawtypes") @@ -40,45 +40,44 @@ private void put( } protected void throwExceptionOnNameCollision( - String newControllerClassName, ControllerConfiguration existing) { + String newReconcilerClassName, ControllerConfiguration existing) { throw new IllegalArgumentException( - "Controller name '" + "Reconciler name '" + existing.getName() + "' is used by both " + existing.getAssociatedReconcilerClassName() + " and " - + newControllerClassName); + + newReconcilerClassName); } @Override public ControllerConfiguration getConfigurationFor( - Reconciler controller) { - final var key = keyFor(controller); + Reconciler reconciler) { + final var key = keyFor(reconciler); final var configuration = configurations.get(key); if (configuration == null) { - logMissingControllerWarning(key, getControllersNameMessage()); + logMissingReconcilerWarning(key, getReconcilersNameMessage()); } return configuration; } - protected void logMissingControllerWarning(String controllerKey, - String controllersNameMessage) { + protected void logMissingReconcilerWarning(String reconcilerKey, String reconcilersNameMessage) { System.out - .println("Cannot find controller named '" + controllerKey + "'. " + controllersNameMessage); + .println("Cannot find reconciler named '" + reconcilerKey + "'. " + reconcilersNameMessage); } - private String getControllersNameMessage() { - return "Known controllers: " - + getKnownControllerNames().stream().reduce((s, s2) -> s + ", " + s2).orElse("None") + private String getReconcilersNameMessage() { + return "Known reconcilers: " + + getKnownReconcilerNames().stream().reduce((s, s2) -> s + ", " + s2).orElse("None") + "."; } - protected String keyFor(Reconciler controller) { - return ControllerUtils.getNameFor(controller); + protected String keyFor(Reconciler reconciler) { + return ReconcilerUtils.getNameFor(reconciler); } - protected ControllerConfiguration getFor(String controllerName) { - return configurations.get(controllerName); + protected ControllerConfiguration getFor(String reconcilerName) { + return configurations.get(reconcilerName); } protected Stream controllerConfigurations() { @@ -86,7 +85,7 @@ protected Stream controllerConfigurations() { } @Override - public Set getKnownControllerNames() { + public Set getKnownReconcilerNames() { return configurations.keySet(); } 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 5715d707a0..894091ad2d 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 @@ -13,9 +13,9 @@ public BaseConfigurationService(Version version) { } @Override - protected void logMissingControllerWarning(String controllerKey, String controllersNameMessage) { - logger.warn("Configuration for controller '{}' was not found. {}", controllerKey, - controllersNameMessage); + protected void logMissingReconcilerWarning(String reconcilerKey, String reconcilersNameMessage) { + logger.warn("Configuration for reconciler '{}' was not found. {}", reconcilerKey, + reconcilersNameMessage); } public String getLoggerName() { 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 bb692e9f9b..2e68d7ff62 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 @@ -30,15 +30,14 @@ public HasMetadata clone(HasMetadata object) { }; /** - * Retrieves the configuration associated with the specified controller + * Retrieves the configuration associated with the specified reconciler * - * @param controller the controller we want the configuration of - * @param the {@code CustomResource} type associated with the specified controller - * @return the {@link ControllerConfiguration} associated with the specified controller or {@code - * null} if no configuration exists for the controller + * @param reconciler the reconciler we want the configuration of + * @param the {@code CustomResource} type associated with the specified reconciler + * @return the {@link ControllerConfiguration} associated with the specified reconciler or {@code + * null} if no configuration exists for the reconciler */ - ControllerConfiguration getConfigurationFor( - Reconciler controller); + ControllerConfiguration getConfigurationFor(Reconciler reconciler); /** * Retrieves the Kubernetes client configuration @@ -51,11 +50,11 @@ default Config getClientConfiguration() { } /** - * Retrieves the set of the names of controllers for which a configuration exists + * Retrieves the set of the names of reconcilers for which a configuration exists * - * @return the set of known controller names + * @return the set of known reconciler names */ - Set getKnownControllerNames(); + Set getKnownReconcilerNames(); /** * Retrieves the {@link Version} information associated with this particular instance of the SDK @@ -67,7 +66,7 @@ default Config getClientConfiguration() { /** * Whether the operator should query the CRD to make sure it's deployed and validate * {@link CustomResource} implementations before attempting to register the associated - * controllers. + * reconcilers. * *

* Note that this might require elevating the privileges associated with the operator to gain read @@ -83,7 +82,7 @@ default boolean checkCRDAndValidateLocalModel() { /** * Retrieves the maximum number of threads the operator can spin out to dispatch reconciliation - * requests to controllers + * requests to reconcilers * * @return the maximum number of concurrent reconciliation threads */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 08adf6e864..17296fce59 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -62,13 +62,13 @@ public ConfigurationService build() { return new ConfigurationService() { @Override public ControllerConfiguration getConfigurationFor( - Reconciler controller) { - return original.getConfigurationFor(controller); + Reconciler reconciler) { + return original.getConfigurationFor(reconciler); } @Override - public Set getKnownControllerNames() { - return original.getKnownControllerNames(); + public Set getKnownReconcilerNames() { + return original.getKnownReconcilerNames(); } @Override 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 dd5eb30cc4..adc1256b79 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 @@ -6,14 +6,14 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilters; public interface ControllerConfiguration { default String getName() { - return ControllerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); + return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); } default String getResourceTypeName() { @@ -21,7 +21,7 @@ default String getResourceTypeName() { } default String getFinalizer() { - return ControllerUtils.getDefaultFinalizerName(getResourceTypeName()); + return ReconcilerUtils.getDefaultFinalizerName(getResourceTypeName()); } /** 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 25472c4626..d7daba49ae 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 @@ -11,11 +11,9 @@ public interface Metrics { default void receivedEvent(Event event) {} - default void reconcileCustomResource(ResourceID resourceID, - RetryInfo retryInfo) {} + default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) {} - default void failedReconciliation(ResourceID resourceID, - RuntimeException exception) {} + default void failedReconciliation(ResourceID resourceID, RuntimeException exception) {} default void cleanupDoneFor(ResourceID customResourceUid) {} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java similarity index 82% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index b8db81c2e9..47870e4f93 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ControllerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -6,13 +6,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -class ControllerUtilsTest { +class ReconcilerUtilsTest { @Test void getDefaultResourceControllerName() { assertEquals( "testcustomreconciler", - ControllerUtils.getDefaultReconcilerName( + ReconcilerUtils.getDefaultReconcilerName( TestCustomReconciler.class.getCanonicalName())); } } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 9a44885a47..fecf2acd15 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -47,7 +47,7 @@ public class OperatorExtension private final KubernetesClient kubernetesClient; private final ConfigurationService configurationService; private final Operator operator; - private final List controllers; + private final List reconcilers; private final boolean preserveNamespaceOnError; private final boolean waitForNamespaceDeletion; @@ -55,13 +55,13 @@ public class OperatorExtension private OperatorExtension( ConfigurationService configurationService, - List controllers, + List reconcilers, boolean preserveNamespaceOnError, boolean waitForNamespaceDeletion) { this.kubernetesClient = new DefaultKubernetesClient(); this.configurationService = configurationService; - this.controllers = controllers; + this.reconcilers = reconcilers; this.operator = new Operator(this.kubernetesClient, this.configurationService); this.preserveNamespaceOnError = preserveNamespaceOnError; this.waitForNamespaceDeletion = waitForNamespaceDeletion; @@ -120,7 +120,7 @@ public T getControllerOfType(Class type) { .map(type::cast) .findFirst() .orElseThrow( - () -> new IllegalArgumentException("Unable to find a controller of type: " + type)); + () -> new IllegalArgumentException("Unable to find a reconciler of type: " + type)); } public NonNamespaceOperation, Resource> resources( @@ -156,8 +156,8 @@ protected void before(ExtensionContext context) { .create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); - for (var ref : controllers) { - final var config = configurationService.getConfigurationFor(ref.controller); + for (var ref : reconcilers) { + final var config = configurationService.getConfigurationFor(ref.reconciler); final var oconfig = override(config).settingNamespace(namespace); final var path = "/META-INF/fabric8/" + config.getResourceTypeName() + "-v1.yml"; @@ -177,12 +177,12 @@ protected void before(ExtensionContext context) { throw new IllegalStateException("Cannot apply CRD yaml: " + path, ex); } - if (ref.controller instanceof KubernetesClientAware) { - ((KubernetesClientAware) ref.controller).setKubernetesClient(kubernetesClient); + if (ref.reconciler instanceof KubernetesClientAware) { + ((KubernetesClientAware) ref.reconciler).setKubernetesClient(kubernetesClient); } - this.operator.register(ref.controller, oconfig.build()); + this.operator.register(ref.reconciler, oconfig.build()); } this.operator.start(); @@ -214,14 +214,14 @@ protected void after(ExtensionContext context) { @SuppressWarnings("rawtypes") public static class Builder { - private final List controllers; + private final List reconcilers; private ConfigurationService configurationService; private boolean preserveNamespaceOnError; private boolean waitForNamespaceDeletion; protected Builder() { this.configurationService = new BaseConfigurationService(Version.UNKNOWN); - this.controllers = new ArrayList<>(); + this.reconcilers = new ArrayList<>(); this.preserveNamespaceOnError = Utils.getSystemPropertyOrEnvVar( "josdk.it.preserveNamespaceOnError", @@ -249,20 +249,20 @@ public Builder withConfigurationService(ConfigurationService value) { @SuppressWarnings("rawtypes") public Builder withReconciler(Reconciler value) { - controllers.add(new ControllerSpec(value, null)); + reconcilers.add(new ReconcilerSpec(value, null)); return this; } @SuppressWarnings("rawtypes") public Builder withReconciler(Reconciler value, Retry retry) { - controllers.add(new ControllerSpec(value, retry)); + reconcilers.add(new ReconcilerSpec(value, retry)); return this; } @SuppressWarnings("rawtypes") public Builder withReconciler(Class value) { try { - controllers.add(new ControllerSpec(value.getConstructor().newInstance(), null)); + reconcilers.add(new ReconcilerSpec(value.getConstructor().newInstance(), null)); } catch (Exception e) { throw new RuntimeException(e); } @@ -272,21 +272,19 @@ public Builder withReconciler(Class value) { public OperatorExtension build() { return new OperatorExtension( configurationService, - controllers, + reconcilers, preserveNamespaceOnError, waitForNamespaceDeletion); } } @SuppressWarnings("rawtypes") - private static class ControllerSpec { - final Reconciler controller; + private static class ReconcilerSpec { + final Reconciler reconciler; final Retry retry; - public ControllerSpec( - Reconciler controller, - Retry retry) { - this.controller = controller; + public ReconcilerSpec(Reconciler reconciler, Retry retry) { + this.reconciler = reconciler; this.retry = retry; } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 976c058c26..22d864158a 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -4,7 +4,7 @@ import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -25,13 +25,13 @@ public AnnotationConfiguration(Reconciler reconciler) { @Override public String getName() { - return ControllerUtils.getNameFor(reconciler); + return ReconcilerUtils.getNameFor(reconciler); } @Override public String getFinalizer() { if (annotation == null || annotation.finalizerName().isBlank()) { - return ControllerUtils.getDefaultFinalizerName(getResourceTypeName()); + return ReconcilerUtils.getDefaultFinalizerName(getResourceTypeName()); } else { return annotation.finalizerName(); } 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 78cb631d63..44fc674028 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 @@ -33,12 +33,12 @@ ControllerConfiguration getConfigurationFor( config = new AnnotationConfiguration<>(reconciler); register(config); getLogger().info( - "Created configuration for controller {} with name {}", + "Created configuration for reconciler {} with name {}", reconciler.getClass().getName(), config.getName()); } } else { - // check that we don't have a controller name collision + // check that we don't have a reconciler name collision final var newControllerClassName = reconciler.getClass().getCanonicalName(); if (!config.getAssociatedReconcilerClassName().equals(newControllerClassName)) { throwExceptionOnNameCollision(newControllerClassName, config); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index fa1101dbd4..9c4bf8c9ab 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -11,7 +11,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; import io.fabric8.kubernetes.model.annotation.Version; -import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -80,7 +80,7 @@ public void returnsValuesFromControllerAnnotationFinalizer() { assertEquals(CustomResource.getCRDName(TestCustomResource.class), configuration.getResourceTypeName()); assertEquals( - ControllerUtils.getDefaultFinalizerName(configuration.getResourceTypeName()), + ReconcilerUtils.getDefaultFinalizerName(configuration.getResourceTypeName()), configuration.getFinalizer()); assertEquals(TestCustomResource.class, configuration.getResourceClass()); assertFalse(configuration.isGenerationAware()); @@ -96,11 +96,11 @@ public void returnCustomerFinalizerNameIfSet() { @Test public void supportsInnerClassCustomResources() { - final var controller = new TestCustomFinalizerReconciler(); + final var reconciler = new TestCustomFinalizerReconciler(); assertDoesNotThrow( () -> { DefaultConfigurationService.instance() - .getConfigurationFor(controller) + .getConfigurationFor(reconciler) .getAssociatedReconcilerClassName(); }); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java index 7ae7e75fe3..3319ecc86c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java @@ -3,7 +3,7 @@ import java.util.concurrent.atomic.AtomicInteger; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; @@ -14,7 +14,7 @@ public class EventSourceTestCustomReconciler TestExecutionInfoProvider { public static final String FINALIZER_NAME = - ControllerUtils.getDefaultFinalizerName( + ReconcilerUtils.getDefaultFinalizerName( CustomResource.getCRDName(EventSourceTestCustomResource.class)); public static final int TIMER_PERIOD = 500; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index 21e44631fb..b8b0cacb04 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -6,7 +6,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -18,7 +18,7 @@ public class RetryTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { public static final String FINALIZER_NAME = - ControllerUtils.getDefaultFinalizerName( + ReconcilerUtils.getDefaultFinalizerName( CustomResource.getCRDName(RetryTestCustomResource.class)); private static final Logger log = LoggerFactory.getLogger(RetryTestCustomReconciler.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index 06efcf8cdd..bd32ad2bd3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -12,7 +12,7 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; @@ -26,7 +26,7 @@ public class TestReconciler private static final Logger log = LoggerFactory.getLogger(TestReconciler.class); public static final String FINALIZER_NAME = - ControllerUtils.getDefaultFinalizerName(CustomResource.getCRDName(TestCustomResource.class)); + ReconcilerUtils.getDefaultFinalizerName(CustomResource.getCRDName(TestCustomResource.class)); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private KubernetesClient kubernetesClient; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index c9e68e0d69..431374392f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -6,7 +6,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.ControllerUtils; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -18,7 +18,7 @@ public class SubResourceTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { public static final String FINALIZER_NAME = - ControllerUtils.getDefaultFinalizerName( + ReconcilerUtils.getDefaultFinalizerName( CustomResource.getCRDName(SubResourceTestCustomResource.class)); private static final Logger log = LoggerFactory.getLogger(SubResourceTestCustomReconciler.class); From 426b129ed526842841cfa7f2554bb77e69e07bb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Dec 2021 08:48:20 +0100 Subject: [PATCH 0180/1608] chore(deps): bump log4j.version from 2.15.0 to 2.16.0 (#752) Bumps `log4j.version` from 2.15.0 to 2.16.0. Updates `log4j-slf4j-impl` from 2.15.0 to 2.16.0 Updates `log4j-core` from 2.15.0 to 2.16.0 Updates `log4j-api` from 2.15.0 to 2.16.0 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-slf4j-impl dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.logging.log4j:log4j-core dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.logging.log4j:log4j-api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fe9a567707..b708b6b6be 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 5.8.2 5.10.1 1.7.32 - 2.15.0 + 2.16.0 4.1.0 3.12.0 1.0.1 From 62262081d57a1d25a3d8c26230d4bbd67d42a3bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 14 Dec 2021 16:01:35 +0100 Subject: [PATCH 0181/1608] fix: remove resource name that pollutes the logs (#753) --- operator-framework-core/src/test/resources/log4j2.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/test/resources/log4j2.xml b/operator-framework-core/src/test/resources/log4j2.xml index 68add1ab41..2892dc78dc 100644 --- a/operator-framework-core/src/test/resources/log4j2.xml +++ b/operator-framework-core/src/test/resources/log4j2.xml @@ -2,7 +2,7 @@ - + From cc335df4e6a897f01fc83881980091f547158d17 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 14 Dec 2021 17:03:20 +0100 Subject: [PATCH 0182/1608] chore(ci): also test kube 1.23 (#754) --- .github/workflows/master-snapshot-release.yml | 4 ++-- .github/workflows/pr.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/master-snapshot-release.yml index abb22c9a65..e754b85894 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/master-snapshot-release.yml @@ -17,7 +17,7 @@ jobs: matrix: java: [ 11, 17 ] distribution: [ temurin ] - kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] + kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1', 'v1.23.0' ] steps: - uses: actions/checkout@v2 - name: Set up Java and Maven @@ -31,7 +31,7 @@ jobs: - name: Set up Minikube uses: manusa/actions-setup-minikube@v2.4.3 with: - minikube version: 'v1.22.0' + minikube version: 'v1.24.0' kubernetes version: ${{ matrix.kubernetes }} driver: 'docker' - name: Run integration tests diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e51c329878..f46bd258f7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,7 +17,7 @@ jobs: matrix: java: [ 11, 17 ] distribution: [ temurin ] - kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1' ] + kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1', 'v1.23.0' ] steps: - uses: actions/checkout@v2 - name: Set up Java and Maven From 2e9bd22dd0b154dfd773503d2da383a568f91433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 15 Dec 2021 10:32:47 +0100 Subject: [PATCH 0183/1608] feat: Event Notification and Polling, Inbound Event Sources (#720) --- operator-framework-core/pom.xml | 16 +- .../api/config/ControllerConfiguration.java | 4 +- .../ControllerConfigurationOverrider.java | 2 +- .../DefaultControllerConfiguration.java | 2 +- .../reconciler/ControllerConfiguration.java | 2 +- .../operator/processing/Controller.java | 4 + .../operator/processing/event/Event.java | 17 ++ .../processing/event/EventProcessor.java | 9 +- .../processing/event/EventSourceManager.java | 28 ++- .../operator/processing/event/ResourceID.java | 3 +- .../event/source/AbstractEventSource.java | 1 + .../event/source/CachingEventSource.java | 72 +++++++ .../processing/event/source/EventSource.java | 7 - .../event/source/EventSourceRegistry.java | 1 + .../source/LifecycleAwareEventSource.java | 22 ++ .../event/source/ResourceAction.java | 5 - .../event/source/ResourceEventAware.java | 13 ++ .../ControllerResourceCache.java | 4 +- .../ControllerResourceEventSource.java | 43 ++-- .../OnceWhitelistEventFilterEventFilter.java | 2 +- .../source/controller/ResourceAction.java | 5 + .../{ => controller}/ResourceCache.java | 2 +- .../{ => controller}/ResourceEvent.java | 2 +- .../{ => controller}/ResourceEventFilter.java | 2 +- .../ResourceEventFilters.java | 2 +- .../inbound/CachingInboundEventSource.java | 21 ++ .../inbound/SimpleInboundEventSource.java | 22 ++ .../{ => informer}/InformerEventSource.java | 3 +- .../event/source/{ => informer}/Mappers.java | 2 +- .../PerResourcePollingEventSource.java | 146 +++++++++++++ .../source/polling/PollingEventSource.java | 68 ++++++ .../source/{ => timer}/TimerEventSource.java | 11 +- .../processing/event/EventProcessorTest.java | 32 +-- .../event/EventSourceManagerTest.java | 15 -- .../event/source/CachingEventSourceTest.java | 97 +++++++++ ...ceWhitelistEventFilterEventFilterTest.java | 1 + .../event/source/ResourceEventFilterTest.java | 14 ++ .../event/source/SampleExternalResource.java | 71 +++++++ .../ControllerResourceEventSourceTest.java | 27 ++- .../PerResourcePollingEventSourceTest.java | 114 ++++++++++ .../polling/PollingEventSourceTest.java | 91 ++++++++ .../{ => timer}/TimerEventSourceTest.java | 4 +- .../runtime/AnnotationConfiguration.java | 4 +- ...formerEventSourceTestCustomReconciler.java | 4 +- pom.xml | 17 ++ sample-operators/mysql-schema/pom.xml | 13 ++ .../sample/MySQLSchemaReconciler.java | 198 +++++++++--------- .../sample/SchemaPollingResourceSupplier.java | 22 ++ .../operator/sample/SchemaStatus.java | 4 +- .../operator/sample/schema/Schema.java | 38 ++++ .../operator/sample/schema/SchemaService.java | 122 +++++++++++ .../operator/sample/TomcatReconciler.java | 2 +- .../operator/sample/WebappReconciler.java | 2 +- 53 files changed, 1221 insertions(+), 214 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceAction.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventAware.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/ControllerResourceCache.java (94%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/ControllerResourceEventSource.java (83%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/OnceWhitelistEventFilterEventFilter.java (94%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceAction.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/ResourceCache.java (89%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/ResourceEvent.java (88%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/ResourceEventFilter.java (97%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/ResourceEventFilters.java (98%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => informer}/InformerEventSource.java (96%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => informer}/Mappers.java (95%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{ => timer}/TimerEventSource.java (84%) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/SampleExternalResource.java rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/{ => controller}/ControllerResourceEventSourceTest.java (86%) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/{ => timer}/TimerEventSourceTest.java (96%) create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index af0737bf29..eafb9ec1b1 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -95,7 +95,6 @@ assertj-core test - io.fabric8 kubernetes-server-mock @@ -106,5 +105,20 @@ awaitility test + + javax.cache + cache-api + ${jcache.version} + + + com.github.ben-manes.caffeine + caffeine + test + + + com.github.ben-manes.caffeine + jcache + test + 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 adc1256b79..31d701c03d 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 @@ -7,8 +7,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; public interface ControllerConfiguration { 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 515addcad0..e8e2ef1162 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 @@ -5,7 +5,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class ControllerConfigurationOverrider { 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 index 965ac32f76..860152745b 100644 --- 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 @@ -4,7 +4,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class DefaultControllerConfiguration implements ControllerConfiguration { 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 7abd5f7b43..b5c6265ae1 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,7 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) 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 1bbb48873c..c8c96cfd6a 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 @@ -212,6 +212,10 @@ private boolean failOnMissingCurrentNS() { return false; } + public EventSourceManager getEventSourceManager() { + return eventSourceManager; + } + public void stop() { if (eventSourceManager != null) { eventSourceManager.stop(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java index deacb85956..0f13b71e11 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.processing.event; +import java.util.Objects; + public class Event { private final ResourceID relatedCustomResource; @@ -18,4 +20,19 @@ public String toString() { "relatedCustomResource=" + relatedCustomResource + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Event event = (Event) o; + return Objects.equals(relatedCustomResource, event.relatedCustomResource); + } + + @Override + public int hashCode() { + return Objects.hash(relatedCustomResource); + } } 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 b3a2fd2896..1f0161aeef 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 @@ -20,10 +20,10 @@ import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.MDCUtils; -import io.javaoperatorsdk.operator.processing.event.source.ResourceAction; -import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEvent; -import io.javaoperatorsdk.operator.processing.event.source.TimerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; +import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.processing.retry.Retry; import io.javaoperatorsdk.operator.processing.retry.RetryExecution; @@ -297,7 +297,6 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) } private void cleanupForDeletedEvent(ResourceID customResourceUid) { - eventSourceManager.cleanupForCustomResource(customResourceUid); eventMarker.cleanup(customResourceUid); metrics.cleanupDoneFor(customResourceUid); } 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 50f2af8641..6848fcbb37 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 @@ -11,10 +11,12 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.LifecycleAware; -import io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.source.TimerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; public class EventSourceManager implements EventSourceRegistry, LifecycleAware { @@ -107,14 +109,22 @@ public final void registerEventSource(EventSource eventSource) } } - public void cleanupForCustomResource(ResourceID customResourceUid) { - lock.lock(); - try { - for (EventSource eventSource : this.eventSources) { - eventSource.cleanupForResource(customResourceUid); + public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldResource) { + for (EventSource eventSource : this.eventSources) { + if (eventSource instanceof ResourceEventAware) { + var lifecycleAwareES = ((ResourceEventAware) eventSource); + switch (action) { + case ADDED: + lifecycleAwareES.onResourceCreated(resource); + break; + case UPDATED: + lifecycleAwareES.onResourceUpdated(resource, oldResource); + break; + case DELETED: + lifecycleAwareES.onResourceDeleted(resource); + break; + } } - } finally { - lock.unlock(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java index 515be245b8..38c9055ff1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java @@ -1,11 +1,12 @@ package io.javaoperatorsdk.operator.processing.event; +import java.io.Serializable; import java.util.Objects; import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -public class ResourceID { +public class ResourceID implements Serializable { public static ResourceID fromResource(HasMetadata resource) { return new ResourceID(resource.getMetadata().getName(), diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java index 555da0d1b1..ddc787ad2d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java @@ -10,4 +10,5 @@ public abstract class AbstractEventSource implements EventSource { public void setEventHandler(EventHandler eventHandler) { this.eventHandler = eventHandler; } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java new file mode 100644 index 0000000000..9a2be41a70 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -0,0 +1,72 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Optional; + +import javax.cache.Cache; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +/** + * Base class for event sources with caching capabilities. + *

+ * {@link #handleDelete(ResourceID)} - if the related resource is present in the cache it is removed + * and event propagated. There is no event propagated if the resource is not in the cache. + *

+ * {@link #handleEvent(Object, ResourceID)} - caches the resource if changed or missing. Propagates + * an event if the resource is new or not equals to the one in the cache, and if accepted by the + * filter if one is present. + * + * @param represents the type of resources (usually external non-kubernetes ones) being handled. + */ +public abstract class CachingEventSource extends LifecycleAwareEventSource { + + private static final Logger log = LoggerFactory.getLogger(CachingEventSource.class); + + protected Cache cache; + + public CachingEventSource(Cache cache) { + this.cache = cache; + } + + protected void handleDelete(ResourceID relatedResourceID) { + if (!isRunning()) { + return; + } + var cachedValue = cache.get(relatedResourceID); + cache.remove(relatedResourceID); + // we only propagate event if the resource was previously in cache + if (cachedValue != null) { + eventHandler.handleEvent(new Event(relatedResourceID)); + } + } + + protected void handleEvent(T value, ResourceID relatedResourceID) { + if (!isRunning()) { + return; + } + var cachedValue = cache.get(relatedResourceID); + if (cachedValue == null || !cachedValue.equals(value)) { + cache.put(relatedResourceID, value); + eventHandler.handleEvent(new Event(relatedResourceID)); + } + } + + public Cache getCache() { + return cache; + } + + public Optional getCachedValue(ResourceID resourceID) { + return Optional.ofNullable(cache.get(resourceID)); + } + + @Override + public void stop() throws OperatorException { + super.stop(); + cache.close(); + } +} 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 e0d0bc45e1..18e47d03db 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 @@ -2,16 +2,9 @@ import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.processing.event.ResourceID; public interface EventSource extends LifecycleAware { void setEventHandler(EventHandler eventHandler); - /** - * Automatically called when a custom resource is deleted from the cluster. - * - * @param customResourceUid - id of custom resource - */ - default void cleanupForResource(ResourceID customResourceUid) {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java index a2d38690cc..dca5436427 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java @@ -4,6 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; public interface EventSourceRegistry { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java new file mode 100644 index 0000000000..6b2d79fd1a --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.javaoperatorsdk.operator.OperatorException; + +public abstract class LifecycleAwareEventSource extends AbstractEventSource { + + private volatile boolean running = false; + + public boolean isRunning() { + return running; + } + + @Override + public void start() throws OperatorException { + running = true; + } + + @Override + public void stop() throws OperatorException { + running = false; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceAction.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceAction.java deleted file mode 100644 index ff0b4673be..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceAction.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -public enum ResourceAction { - ADDED, UPDATED, DELETED -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventAware.java new file mode 100644 index 0000000000..dcb15a4229 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventAware.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface ResourceEventAware { + + default void onResourceCreated(T resource) {} + + default void onResourceUpdated(T newResource, T oldResource) {} + + default void onResourceDeleted(T resource) {} + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java similarity index 94% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceCache.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java index 2186277e32..2397af9573 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import java.util.Map; import java.util.Optional; @@ -11,7 +11,7 @@ import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import static io.javaoperatorsdk.operator.processing.event.source.ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY; +import static io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY; public class ControllerResourceCache implements ResourceCache { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java similarity index 83% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index faf0d8f33b..2386cd4dbc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import java.util.Collections; import java.util.Map; @@ -20,6 +20,7 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; @@ -75,30 +76,20 @@ public void start() { try { if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { - final var filteredBySelectorClient = client.inAnyNamespace() - .withLabelSelector(labelSelector); final var informer = - createAndRunInformerFor(filteredBySelectorClient, ANY_NAMESPACE_MAP_KEY); + createAndRunInformerFor(client.inAnyNamespace() + .withLabelSelector(labelSelector), ANY_NAMESPACE_MAP_KEY); log.debug("Registered {} -> {} for any namespace", controller, informer); } else { - targetNamespaces.forEach( - ns -> { - final var informer = createAndRunInformerFor( - client.inNamespace(ns).withLabelSelector(labelSelector), ns); - log.debug("Registered {} -> {} for namespace: {}", controller, informer, - ns); - }); + targetNamespaces.forEach(ns -> { + final var informer = createAndRunInformerFor( + client.inNamespace(ns).withLabelSelector(labelSelector), ns); + log.debug("Registered {} -> {} for namespace: {}", controller, informer, ns); + }); } } catch (Exception e) { if (e instanceof KubernetesClientException) { - KubernetesClientException ke = (KubernetesClientException) e; - if (404 == ke.getCode()) { - // only throw MissingCRDException if the 404 error occurs on the target CRD - final var targetCRDName = controller.getConfiguration().getResourceTypeName(); - if (targetCRDName.equals(ke.getFullResourceName())) { - throw new MissingCRDException(targetCRDName, null, e.getMessage(), e); - } - } + handleKubernetesClientException(e); } throw e; } @@ -130,6 +121,8 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource log.debug( "Event received for resource: {}", getName(customResource)); MDCUtils.addResourceInfo(customResource); + controller.getEventSourceManager().broadcastOnResourceEvent(action, customResource, + oldResource); if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { eventHandler.handleEvent( new ResourceEvent(action, ResourceID.fromResource(customResource))); @@ -191,4 +184,16 @@ public void whitelistNextEvent(ResourceID resourceID) { } } + + private void handleKubernetesClientException(Exception e) { + KubernetesClientException ke = (KubernetesClientException) e; + if (404 == ke.getCode()) { + // only throw MissingCRDException if the 404 error occurs on the target CRD + final var targetCRDName = controller.getConfiguration().getResourceTypeName(); + if (targetCRDName.equals(ke.getFullResourceName())) { + throw new MissingCRDException(targetCRDName, null, e.getMessage(), e); + } + } + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java similarity index 94% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilter.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java index aba822dcf6..8262ff1c21 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceAction.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceAction.java new file mode 100644 index 0000000000..7a04dc9164 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceAction.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.processing.event.source.controller; + +public enum ResourceAction { + ADDED, UPDATED, DELETED +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceCache.java similarity index 89% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceCache.java index af156e24ec..5508dd43eb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceCache.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import java.util.Optional; import java.util.function.Predicate; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java similarity index 88% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEvent.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java index 7610a880b7..ad1d85330c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java similarity index 97% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilter.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java index 01f7e1aec0..497c9016b7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java similarity index 98% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilters.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java index 9d90ebd963..43fe410fbc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java new file mode 100644 index 0000000000..51d1ed287d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.processing.event.source.inbound; + +import javax.cache.Cache; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; + +public class CachingInboundEventSource extends CachingEventSource { + + public CachingInboundEventSource(Cache cache) { + super(cache); + } + + public void handleResourceEvent(T resource, ResourceID relatedResourceID) { + super.handleEvent(resource, relatedResourceID); + } + + public void handleResourceDeleteEvent(ResourceID resourceID) { + super.handleDelete(resourceID); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java new file mode 100644 index 0000000000..475cfee916 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.processing.event.source.inbound; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.LifecycleAwareEventSource; + +public class SimpleInboundEventSource extends LifecycleAwareEventSource { + + private static final Logger log = LoggerFactory.getLogger(SimpleInboundEventSource.class); + + public void propagateEvent(ResourceID resourceID) { + if (isRunning()) { + eventHandler.handleEvent(new Event(resourceID)); + } else { + log.debug("Event source not started yet, not propagating event for: {}", resourceID); + } + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java similarity index 96% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 3bdc8fb764..8095289221 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Objects; import java.util.Set; @@ -15,6 +15,7 @@ import io.fabric8.kubernetes.client.informers.cache.Store; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; public class InformerEventSource extends AbstractEventSource { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java similarity index 95% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Mappers.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index d428bb26cc..c578490147 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Collections; import java.util.Set; 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 new file mode 100644 index 0000000000..bf9b41cf0e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java @@ -0,0 +1,146 @@ +package io.javaoperatorsdk.operator.processing.event.source.polling; + +import java.util.Map; +import java.util.Optional; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +import javax.cache.Cache; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; + +/** + * + * Polls the supplier for each controlled resource registered. Resource is registered when created + * if there is no registerPredicate provided. If register predicate provided it is evaluated on + * resource create and/or update to register polling for the event source. + *

+ * For other behavior see {@link CachingEventSource} + * + * @param the resource polled by the event source + * @param related custom resource + */ +public class PerResourcePollingEventSource + extends CachingEventSource + implements ResourceEventAware { + + private static final Logger log = LoggerFactory.getLogger(PerResourcePollingEventSource.class); + + private final Timer timer = new Timer(); + private final Map timerTasks = new ConcurrentHashMap<>(); + private final ResourceSupplier resourceSupplier; + private final ResourceCache resourceCache; + private final Predicate registerPredicate; + private final long period; + + public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, + ResourceCache resourceCache, long period, Cache cache) { + this(resourceSupplier, resourceCache, period, cache, null); + } + + public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, + ResourceCache resourceCache, long period, Cache cache, + Predicate registerPredicate) { + super(cache); + this.resourceSupplier = resourceSupplier; + this.resourceCache = resourceCache; + this.period = period; + this.registerPredicate = registerPredicate; + } + + private void pollForResource(R resource) { + var value = resourceSupplier.getResources(resource); + var resourceID = ResourceID.fromResource(resource); + if (value.isEmpty()) { + super.handleDelete(resourceID); + } else { + super.handleEvent(value.get(), resourceID); + } + } + + private Optional getAndCacheResource(ResourceID resourceID) { + var resource = resourceCache.get(resourceID); + if (resource.isPresent()) { + var value = resourceSupplier.getResources(resource.get()); + value.ifPresent(v -> cache.put(resourceID, v)); + return value; + } + return Optional.empty(); + } + + @Override + public void onResourceCreated(R resource) { + checkAndRegisterTask(resource); + } + + @Override + public void onResourceUpdated(R newResource, R oldResource) { + checkAndRegisterTask(newResource); + } + + @Override + public void onResourceDeleted(R resource) { + var resourceID = ResourceID.fromResource(resource); + TimerTask task = timerTasks.remove(resourceID); + if (task != null) { + task.cancel(); + } + cache.remove(resourceID); + } + + private void checkAndRegisterTask(R resource) { + var resourceID = ResourceID.fromResource(resource); + if (timerTasks.get(resourceID) == null && (registerPredicate == null + || registerPredicate.test(resource))) { + timer.schedule(new TimerTask() { + @Override + public void run() { + if (!isRunning()) { + log.debug("Event source not yet started. Will not run for: {}", resourceID); + return; + } + // always use up-to-date resource from cache + var res = resourceCache.get(resourceID); + res.ifPresentOrElse(r -> pollForResource(r), + () -> log.warn("No resource in cache for resource ID: {}", resourceID)); + } + }, 0, period); + } + } + + /** + * + * @param resourceID of the target related resource + * @return the cached value of the resource, if not present it gets the resource from the + * supplier. The value provided from the supplier is cached, but no new event is + * propagated. + */ + public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { + var cachedValue = getCachedValue(resourceID); + if (cachedValue.isPresent()) { + return cachedValue; + } else { + return getAndCacheResource(resourceID); + } + } + + public interface ResourceSupplier { + Optional getResources(R resource); + } + + @Override + 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 new file mode 100644 index 0000000000..b2c3fdff78 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java @@ -0,0 +1,68 @@ +package io.javaoperatorsdk.operator.processing.event.source.polling; + +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.StreamSupport; + +import javax.cache.Cache; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; + +public class PollingEventSource extends CachingEventSource { + + private static final Logger log = LoggerFactory.getLogger(PollingEventSource.class); + + private final Timer timer = new Timer(); + private final Supplier> supplierToPoll; + private final long period; + + public PollingEventSource(Supplier> supplier, + long period, Cache cache) { + super(cache); + this.supplierToPoll = supplier; + this.period = period; + } + + @Override + public void start() throws OperatorException { + super.start(); + timer.schedule(new TimerTask() { + @Override + public void run() { + if (!isRunning()) { + log.debug("Event source not yet started. Will not run."); + return; + } + getStateAndFillCache(); + } + }, period, period); + } + + protected void getStateAndFillCache() { + var values = supplierToPoll.get(); + values.forEach((k, v) -> super.handleEvent(v, k)); + StreamSupport.stream(cache.spliterator(), false) + .filter(e -> !values.containsKey(e.getKey())).map(Cache.Entry::getKey) + .forEach(super::handleDelete); + } + + @Override + public void stop() throws OperatorException { + super.stop(); + timer.cancel(); + } + + public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { + var resource = getCachedValue(resourceID); + if (resource.isPresent()) { + return resource; + } + getStateAndFillCache(); + return getCachedValue(resourceID); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java similarity index 84% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java index ebac87cdf2..2ab8b2f128 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.timer; import java.util.Map; import java.util.Timer; @@ -12,8 +12,11 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; -public class TimerEventSource extends AbstractEventSource { +public class TimerEventSource extends AbstractEventSource + implements ResourceEventAware { private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class); private final Timer timer = new Timer(); @@ -35,8 +38,8 @@ public void scheduleOnce(R resource, long delay) { } @Override - public void cleanupForResource(ResourceID resourceUid) { - cancelOnceSchedule(resourceUid); + public void onResourceDeleted(R resource) { + cancelOnceSchedule(ResourceID.fromResource(resource)); } public void cancelOnceSchedule(ResourceID customResourceUid) { 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 9f99b1bdd0..f837f5eea2 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 @@ -12,12 +12,15 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.source.*; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; +import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; -import static io.javaoperatorsdk.operator.processing.event.source.ResourceAction.DELETED; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; @@ -205,31 +208,6 @@ public void doNotFireEventsIfClosing() { verify(reconciliationDispatcherMock, timeout(50).times(0)).handleExecution(any()); } - @Test - public void cleansUpWhenDeleteEventReceivedAndNoEventPresent() { - Event deleteEvent = - new ResourceEvent(DELETED, prepareCREvent().getRelatedCustomResourceID()); - - eventProcessor.handleEvent(deleteEvent); - - verify(eventSourceManagerMock, times(1)) - .cleanupForCustomResource(eq(deleteEvent.getRelatedCustomResourceID())); - } - - @Test - public void cleansUpAfterExecutionIfOnlyDeleteEventMarkLeft() { - var cr = testCustomResource(); - var crEvent = prepareCREvent(ResourceID.fromResource(cr)); - eventProcessor.getEventMarker().markDeleteEventReceived(crEvent.getRelatedCustomResourceID()); - var executionScope = new ExecutionScope(cr, null); - - eventProcessor.eventProcessingFinished(executionScope, - PostExecutionControl.defaultDispatch()); - - verify(eventSourceManagerMock, times(1)) - .cleanupForCustomResource(eq(crEvent.getRelatedCustomResourceID())); - } - @Test public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index 164a66b970..79311a7057 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -5,8 +5,6 @@ import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import static org.assertj.core.api.Assertions.assertThat; @@ -57,17 +55,4 @@ public void startCascadesToEventSources() { verify(eventSource, times(1)).start(); verify(eventSource2, times(1)).start(); } - - @Test - public void deRegistersEventSources() { - CustomResource customResource = TestUtils.testCustomResource(); - EventSource eventSource = mock(EventSource.class); - eventSourceManager.registerEventSource(eventSource); - - eventSourceManager - .cleanupForCustomResource(ResourceID.fromResource(customResource)); - - verify(eventSource, times(1)) - .cleanupForResource(eq(ResourceID.fromResource(customResource))); - } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java new file mode 100644 index 0000000000..15fcca7253 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java @@ -0,0 +1,97 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; + +import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class CachingEventSourceTest { + + private CachingEventSource cachingEventSource; + private Cache cache; + private EventHandler eventHandlerMock = mock(EventHandler.class); + + @BeforeEach + public void setup() { + CachingProvider cachingProvider = new CaffeineCachingProvider(); + CacheManager cacheManager = cachingProvider.getCacheManager(); + cache = cacheManager.createCache("test-caching", new MutableConfiguration<>()); + + cachingEventSource = new SimpleCachingEventSource(cache); + cachingEventSource.setEventHandler(eventHandlerMock); + cachingEventSource.start(); + } + + @AfterEach + public void tearDown() { + cachingEventSource.stop(); + } + + @Test + public void putsNewResourceIntoCacheAndProducesEvent() { + cachingEventSource.handleEvent(testResource1(), testResource1ID()); + + verify(eventHandlerMock, times(1)).handleEvent(eq(new Event(testResource1ID()))); + assertThat(cachingEventSource.getCachedValue(testResource1ID())).isPresent(); + } + + @Test + public void propagatesEventIfResourceChanged() { + var res2 = testResource1(); + res2.setValue("changedValue"); + cachingEventSource.handleEvent(testResource1(), testResource1ID()); + cachingEventSource.handleEvent(res2, testResource1ID()); + + + verify(eventHandlerMock, times(2)).handleEvent(eq(new Event(testResource1ID()))); + assertThat(cachingEventSource.getCachedValue(testResource1ID()).get()).isEqualTo(res2); + } + + @Test + public void noEventPropagatedIfTheResourceIsNotChanged() { + cachingEventSource.handleEvent(testResource1(), testResource1ID()); + cachingEventSource.handleEvent(testResource1(), testResource1ID()); + + verify(eventHandlerMock, times(1)).handleEvent(eq(new Event(testResource1ID()))); + assertThat(cachingEventSource.getCachedValue(testResource1ID())).isPresent(); + } + + @Test + public void propagatesEventOnDeleteIfThereIsPrevResourceInCache() { + cachingEventSource.handleEvent(testResource1(), testResource1ID()); + cachingEventSource.handleDelete(testResource1ID()); + + verify(eventHandlerMock, times(2)).handleEvent(eq(new Event(testResource1ID()))); + assertThat(cachingEventSource.getCachedValue(testResource1ID())).isNotPresent(); + } + + @Test + public void noEventOnDeleteIfResourceWasNotInCacheBefore() { + cachingEventSource.handleDelete(testResource1ID()); + + verify(eventHandlerMock, times(0)).handleEvent(eq(new Event(testResource1ID()))); + } + + + public static class SimpleCachingEventSource + extends CachingEventSource { + public SimpleCachingEventSource(Cache cache) { + super(cache); + } + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java index 4595f1b649..3d2582fd3e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java @@ -4,6 +4,7 @@ import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.controller.OnceWhitelistEventFilterEventFilter; import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; import static org.assertj.core.api.Assertions.assertThat; 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 571d9433c6..be6d9f803b 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 @@ -17,6 +17,10 @@ import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -195,6 +199,11 @@ public TestController(ControllerConfiguration configuration) super(null, configuration, null); } + @Override + public EventSourceManager getEventSourceManager() { + return mock(EventSourceManager.class); + } + @Override public MixedOperation, Resource> getCRClient() { return mock(MixedOperation.class); @@ -209,6 +218,11 @@ public ObservedGenController( super(null, configuration, null); } + @Override + public EventSourceManager getEventSourceManager() { + return mock(EventSourceManager.class); + } + @Override public MixedOperation, Resource> getCRClient() { return mock(MixedOperation.class); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/SampleExternalResource.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/SampleExternalResource.java new file mode 100644 index 0000000000..bf2ff42c96 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/SampleExternalResource.java @@ -0,0 +1,71 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.io.Serializable; +import java.util.Objects; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class SampleExternalResource implements Serializable { + + public static final String DEFAULT_VALUE_1 = "value1"; + public static final String DEFAULT_VALUE_2 = "value2"; + public static final String NAME_1 = "name1"; + public static final String NAME_2 = "name2"; + + public static SampleExternalResource testResource1() { + return new SampleExternalResource(NAME_1, DEFAULT_VALUE_1); + } + + public static SampleExternalResource testResource2() { + return new SampleExternalResource(NAME_2, DEFAULT_VALUE_2); + } + + public static ResourceID testResource1ID() { + return new ResourceID(NAME_1, "testns"); + } + + public static ResourceID testResource2ID() { + return new ResourceID(NAME_2, "testns"); + } + + private String name; + private String value; + + public SampleExternalResource(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public SampleExternalResource setName(String name) { + this.name = name; + return this; + } + + public String getValue() { + return value; + } + + public SampleExternalResource setValue(String value) { + this.value = value; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + SampleExternalResource that = (SampleExternalResource) o; + return Objects.equals(name, that.name) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, value); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java similarity index 86% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSourceTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index e63c415e20..5eb26db524 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.controller; import java.time.LocalDateTime; import java.util.List; @@ -15,9 +15,11 @@ import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -31,8 +33,9 @@ class ControllerResourceEventSourceTest { mock(MixedOperation.class); EventHandler eventHandler = mock(EventHandler.class); + private TestController testController = new TestController(true); private ControllerResourceEventSource controllerResourceEventSource = - new ControllerResourceEventSource<>(new TestController(true)); + new ControllerResourceEventSource<>(testController); @BeforeEach public void setup() { @@ -136,12 +139,32 @@ public void notHandlesNextEventIfNotWhitelisted() { verify(eventHandler, times(0)).handleEvent(any()); } + @Test + public void callsBroadcastsOnResourceEvents() { + TestCustomResource customResource1 = TestUtils.testCustomResource(); + + controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, + customResource1); + + verify(testController.getEventSourceManager(), times(1)) + .broadcastOnResourceEvent(eq(ResourceAction.UPDATED), eq(customResource1), + eq(customResource1)); + } + private static class TestController extends Controller { + private EventSourceManager eventSourceManager = + mock(EventSourceManager.class); + public TestController(boolean generationAware) { super(null, new TestConfiguration(generationAware), null); } + @Override + public EventSourceManager getEventSourceManager() { + return eventSourceManager; + } + @Override public MixedOperation, Resource> getCRClient() { return client; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java new file mode 100644 index 0000000000..6eb4b98e3b --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java @@ -0,0 +1,114 @@ +package io.javaoperatorsdk.operator.processing.event.source.polling; + +import java.util.Optional; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class PerResourcePollingEventSourceTest { + + public static final int PERIOD = 80; + private PerResourcePollingEventSource pollingEventSource; + private PerResourcePollingEventSource.ResourceSupplier supplier = + mock(PerResourcePollingEventSource.ResourceSupplier.class); + private ResourceCache resourceCache = mock(ResourceCache.class); + private Cache cache; + private EventHandler eventHandler = mock(EventHandler.class); + private TestCustomResource testCustomResource = TestUtils.testCustomResource(); + + @BeforeEach + public void setup() { + CachingProvider cachingProvider = new CaffeineCachingProvider(); + CacheManager cacheManager = cachingProvider.getCacheManager(); + cache = cacheManager.createCache("test-caching", new MutableConfiguration<>()); + + when(resourceCache.get(any())).thenReturn(Optional.of(testCustomResource)); + when(supplier.getResources(any())) + .thenReturn(Optional.of(SampleExternalResource.testResource1())); + + pollingEventSource = + new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, cache); + pollingEventSource.setEventHandler(eventHandler); + } + + @Test + public void pollsTheResourceAfterAwareOfIt() throws InterruptedException { + pollingEventSource.start(); + pollingEventSource.onResourceCreated(testCustomResource); + + Thread.sleep(3 * PERIOD); + verify(supplier, atLeast(2)).getResources(eq(testCustomResource)); + verify(eventHandler, times(1)).handleEvent(any()); + } + + @Test + public void registeringTaskOnAPredicate() throws InterruptedException { + pollingEventSource = new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, cache, + testCustomResource -> testCustomResource.getMetadata().getGeneration() > 1); + pollingEventSource.setEventHandler(eventHandler); + pollingEventSource.start(); + pollingEventSource.onResourceCreated(testCustomResource); + Thread.sleep(2 * PERIOD); + + verify(supplier, times(0)).getResources(eq(testCustomResource)); + testCustomResource.getMetadata().setGeneration(2L); + pollingEventSource.onResourceUpdated(testCustomResource, testCustomResource); + + Thread.sleep(2 * PERIOD); + + verify(supplier, atLeast(1)).getResources(eq(testCustomResource)); + } + + @Test + public void propagateEventOnDeletedResource() throws InterruptedException { + pollingEventSource.start(); + pollingEventSource.onResourceCreated(testCustomResource); + when(supplier.getResources(any())) + .thenReturn(Optional.of(SampleExternalResource.testResource1())) + .thenReturn(Optional.empty()); + + Thread.sleep(3 * PERIOD); + verify(supplier, atLeast(2)).getResources(eq(testCustomResource)); + verify(eventHandler, times(2)).handleEvent(any()); + } + + @Test + public void getsValueFromCacheOrSupplier() throws InterruptedException { + pollingEventSource.start(); + pollingEventSource.onResourceCreated(testCustomResource); + when(supplier.getResources(any())) + .thenReturn(Optional.empty()) + .thenReturn(Optional.of(SampleExternalResource.testResource1())); + + Thread.sleep(PERIOD / 2); + + var value = + pollingEventSource.getValueFromCacheOrSupplier(ResourceID.fromResource(testCustomResource)); + + Thread.sleep(PERIOD * 2); + + assertThat(value).isPresent(); + verify(eventHandler, never()).handleEvent(any()); + } + +} 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 new file mode 100644 index 0000000000..177760f94e --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSourceTest.java @@ -0,0 +1,91 @@ +package io.javaoperatorsdk.operator.processing.event.source.polling; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; + +import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; + +import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; +import static org.mockito.Mockito.*; + +class PollingEventSourceTest { + + private PollingEventSource pollingEventSource; + private Supplier> supplier = mock(Supplier.class); + private Cache cache; + private EventHandler eventHandler = mock(EventHandler.class); + + @BeforeEach + public void setup() { + CachingProvider cachingProvider = new CaffeineCachingProvider(); + CacheManager cacheManager = cachingProvider.getCacheManager(); + cache = cacheManager.createCache("test-caching", new MutableConfiguration<>()); + + pollingEventSource = new PollingEventSource<>(supplier, 50, cache); + pollingEventSource.setEventHandler(eventHandler); + } + + @AfterEach + public void teardown() { + pollingEventSource.stop(); + } + + @Test + public void pollsAndProcessesEvents() throws InterruptedException { + when(supplier.get()).thenReturn(testResponseWithTwoValues()); + pollingEventSource.start(); + + Thread.sleep(100); + + verify(eventHandler, times(2)).handleEvent(any()); + } + + @Test + public void propagatesEventForRemovedResources() throws InterruptedException { + when(supplier.get()).thenReturn(testResponseWithTwoValues()) + .thenReturn(testResponseWithOneValue()); + pollingEventSource.start(); + + Thread.sleep(150); + + verify(eventHandler, times(3)).handleEvent(any()); + } + + @Test + public void doesNotPropagateEventIfResourceNotChanged() throws InterruptedException { + when(supplier.get()).thenReturn(testResponseWithTwoValues()); + pollingEventSource.start(); + + Thread.sleep(250); + + verify(eventHandler, times(2)).handleEvent(any()); + } + + private Map testResponseWithOneValue() { + Map res = new HashMap<>(); + res.put(testResource1ID(), testResource1()); + return res; + } + + private Map testResponseWithTwoValues() { + Map res = new HashMap<>(); + res.put(testResource1ID(), testResource1()); + res.put(testResource2ID(), testResource2()); + return res; + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java similarity index 96% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSourceTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java index d1768a8547..0f259f77cb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source; +package io.javaoperatorsdk.operator.processing.event.source.timer; import java.io.IOException; import java.util.List; @@ -73,7 +73,7 @@ public void deRegistersOnceEventSources() { timerEventSource.scheduleOnce(customResource, PERIOD); timerEventSource - .cleanupForResource(ResourceID.fromResource(customResource)); + .onResourceDeleted(customResource); untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 22d864158a..9a5577a90d 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -8,8 +8,8 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventFilters; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; public class AnnotationConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 71463e13d0..b6807358f1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -12,8 +12,8 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.source.InformerEventSource; -import io.javaoperatorsdk.operator.processing.event.source.Mappers; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; diff --git a/pom.xml b/pom.xml index b708b6b6be..4248fca4ce 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,8 @@ 2.17.1 1.0 1.6.2 + 1.1.1 + 3.0.4 @@ -168,6 +170,21 @@ operator-framework ${project.version} + + javax.cache + cache-api + ${jcache.version} + + + com.github.ben-manes.caffeine + caffeine + ${caffein.version} + + + com.github.ben-manes.caffeine + jcache + ${caffein.version} + diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 8693cf6154..f5df325685 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -67,6 +67,19 @@ jackson-dataformat-yaml 2.13.0 + + javax.cache + cache-api + ${jcache.version} + + + com.github.ben-manes.caffeine + caffeine + + + com.github.ben-manes.caffeine + jcache + diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 47d44246e3..33b8df52df 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -2,128 +2,102 @@ import java.sql.Connection; import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import java.util.Base64; +import java.util.Optional; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.sample.schema.Schema; +import io.javaoperatorsdk.operator.sample.schema.SchemaService; + +import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; import static java.lang.String.format; @ControllerConfiguration -public class MySQLSchemaReconciler implements Reconciler { - static final String USERNAME_FORMAT = "%s-user"; - static final String SECRET_FORMAT = "%s-secret"; - +public class MySQLSchemaReconciler + implements Reconciler, ErrorStatusHandler, + EventSourceInitializer { + public static final String SECRET_FORMAT = "%s-secret"; + public static final String USERNAME_FORMAT = "%s-user"; + public static final int POLL_PERIOD = 500; private final Logger log = LoggerFactory.getLogger(getClass()); private final KubernetesClient kubernetesClient; private final MySQLDbConfig mysqlDbConfig; + PerResourcePollingEventSource perResourcePollingEventSource; public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig mysqlDbConfig) { this.kubernetesClient = kubernetesClient; this.mysqlDbConfig = mysqlDbConfig; } + @Override + public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + CachingProvider cachingProvider = new CaffeineCachingProvider(); + CacheManager cacheManager = cachingProvider.getCacheManager(); + Cache schemaCache = + cacheManager.createCache("schema-cache", new MutableConfiguration<>()); + + perResourcePollingEventSource = + new PerResourcePollingEventSource<>(new SchemaPollingResourceSupplier(mysqlDbConfig), + eventSourceRegistry.getControllerResourceEventSource().getResourceCache(), POLL_PERIOD, + schemaCache); + + eventSourceRegistry.registerEventSource(perResourcePollingEventSource); + } + @Override public UpdateControl reconcile(MySQLSchema schema, Context context) { + var dbSchema = perResourcePollingEventSource + .getValueFromCacheOrSupplier(ResourceID.fromResource(schema)); try (Connection connection = getConnection()) { - if (!schemaExists(connection, schema.getMetadata().getName())) { - try (Statement statement = connection.createStatement()) { - statement.execute( - format( - "CREATE SCHEMA `%1$s` DEFAULT CHARACTER SET %2$s", - schema.getMetadata().getName(), schema.getSpec().getEncoding())); - } - + if (!dbSchema.isPresent()) { + var schemaName = schema.getMetadata().getName(); String password = RandomStringUtils.randomAlphanumeric(16); - String userName = String.format(USERNAME_FORMAT, schema.getMetadata().getName()); - String secretName = String.format(SECRET_FORMAT, schema.getMetadata().getName()); - try (Statement statement = connection.createStatement()) { - statement.execute(format("CREATE USER '%1$s' IDENTIFIED BY '%2$s'", userName, password)); - } - try (Statement statement = connection.createStatement()) { - statement.execute( - format("GRANT ALL ON `%1$s`.* TO '%2$s'", schema.getMetadata().getName(), userName)); - } - Secret credentialsSecret = - new SecretBuilder() - .withNewMetadata() - .withName(secretName) - .endMetadata() - .addToData( - "MYSQL_USERNAME", Base64.getEncoder().encodeToString(userName.getBytes())) - .addToData( - "MYSQL_PASSWORD", Base64.getEncoder().encodeToString(password.getBytes())) - .build(); - this.kubernetesClient - .secrets() - .inNamespace(schema.getMetadata().getNamespace()) - .create(credentialsSecret); - - SchemaStatus status = new SchemaStatus(); - status.setUrl( - format( - "jdbc:mysql://%1$s/%2$s", - System.getenv("MYSQL_HOST"), schema.getMetadata().getName())); - status.setUserName(userName); - status.setSecretName(secretName); - status.setStatus("CREATED"); - schema.setStatus(status); - log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); + String secretName = String.format(SECRET_FORMAT, schemaName); + String userName = String.format(USERNAME_FORMAT, schemaName); + SchemaService.createSchemaAndRelatedUser(connection, schemaName, + schema.getSpec().getEncoding(), userName, password); + createSecret(schema, password, secretName, userName); + updateStatusPojo(schema, secretName, userName); + log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); return UpdateControl.updateStatus(schema); } return UpdateControl.noUpdate(); } catch (SQLException e) { log.error("Error while creating Schema", e); - - SchemaStatus status = new SchemaStatus(); - status.setUrl(null); - status.setUserName(null); - status.setSecretName(null); - status.setStatus("ERROR: " + e.getMessage()); - schema.setStatus(status); - - return UpdateControl.updateStatus(schema); + throw new IllegalStateException(e); } } @Override public DeleteControl cleanup(MySQLSchema schema, Context context) { log.info("Execution deleteResource for: {}", schema.getMetadata().getName()); - try (Connection connection = getConnection()) { - if (schemaExists(connection, schema.getMetadata().getName())) { - try (Statement statement = connection.createStatement()) { - statement.execute(format("DROP DATABASE `%1$s`", schema.getMetadata().getName())); - } - log.info("Deleted Schema '{}'", schema.getMetadata().getName()); - - if (schema.getStatus() != null) { - if (userExists(connection, schema.getStatus().getUserName())) { - try (Statement statement = connection.createStatement()) { - statement.execute(format("DROP USER '%1$s'", schema.getStatus().getUserName())); - } - log.info("Deleted User '{}'", schema.getStatus().getUserName()); - } - } - - this.kubernetesClient - .secrets() - .inNamespace(schema.getMetadata().getNamespace()) - .withName(schema.getStatus().getSecretName()) - .delete(); + var dbSchema = SchemaService.getSchema(connection, schema.getMetadata().getName()); + if (dbSchema.isPresent()) { + var userName = schema.getStatus() != null ? schema.getStatus().getUserName() : null; + SchemaService.deleteSchemaAndRelatedUser(connection, schema.getMetadata().getName(), + userName); } else { log.info( "Delete event ignored for schema '{}', real schema doesn't exist", @@ -136,33 +110,65 @@ public DeleteControl cleanup(MySQLSchema schema, Context context) { } } + @Override + public Optional updateErrorStatus(MySQLSchema schema, RetryInfo retryInfo, + RuntimeException e) { + SchemaStatus status = new SchemaStatus(); + status.setUrl(null); + status.setUserName(null); + status.setSecretName(null); + status.setStatus("ERROR: " + e.getMessage()); + schema.setStatus(status); + return Optional.empty(); + } + private Connection getConnection() throws SQLException { String connectionString = format("jdbc:mysql://%1$s:%2$s", mysqlDbConfig.getHost(), mysqlDbConfig.getPort()); - log.info("Connecting to '{}' with user '{}'", connectionString, mysqlDbConfig.getUser()); + log.debug("Connecting to '{}' with user '{}'", connectionString, mysqlDbConfig.getUser()); return DriverManager.getConnection(connectionString, mysqlDbConfig.getUser(), mysqlDbConfig.getPassword()); } - private boolean schemaExists(Connection connection, String schemaName) throws SQLException { - try (PreparedStatement ps = - connection.prepareStatement( - "SELECT schema_name FROM information_schema.schemata WHERE schema_name = ?")) { - ps.setString(1, schemaName); - try (ResultSet resultSet = ps.executeQuery()) { - return resultSet.next(); - } - } + private void updateStatusPojo(MySQLSchema schema, String secretName, String userName) { + SchemaStatus status = new SchemaStatus(); + status.setUrl( + format( + "jdbc:mysql://%1$s/%2$s", + System.getenv("MYSQL_HOST"), schema.getMetadata().getName())); + status.setUserName(userName); + status.setSecretName(secretName); + status.setStatus("CREATED"); + schema.setStatus(status); } - private boolean userExists(Connection connection, String userName) throws SQLException { - try (PreparedStatement ps = - connection.prepareStatement("SELECT User FROM mysql.user WHERE User = ?")) { - ps.setString(1, userName); - try (ResultSet resultSet = ps.executeQuery()) { - return resultSet.first(); - } + private void createSecret(MySQLSchema schema, String password, String secretName, + String userName) { + + var currentSecret = kubernetesClient.secrets().inNamespace(schema.getMetadata().getNamespace()) + .withName(secretName).get(); + if (currentSecret != null) { + return; } + Secret credentialsSecret = + new SecretBuilder() + .withNewMetadata() + .withName(secretName) + .withOwnerReferences(new OwnerReference("mysql.sample.javaoperatorsdk/v1", + false, false, "MySQLSchema", + schema.getMetadata().getName(), schema.getMetadata().getUid())) + .endMetadata() + .addToData( + "MYSQL_USERNAME", Base64.getEncoder().encodeToString(userName.getBytes())) + .addToData( + "MYSQL_PASSWORD", Base64.getEncoder().encodeToString(password.getBytes())) + .build(); + this.kubernetesClient + .secrets() + .inNamespace(schema.getMetadata().getNamespace()) + .create(credentialsSecret); } + + } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java new file mode 100644 index 0000000000..52f3f301c8 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.Optional; + +import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.sample.schema.Schema; +import io.javaoperatorsdk.operator.sample.schema.SchemaService; + +public class SchemaPollingResourceSupplier + implements PerResourcePollingEventSource.ResourceSupplier { + + private final SchemaService schemaService; + + public SchemaPollingResourceSupplier(MySQLDbConfig mySQLDbConfig) { + this.schemaService = new SchemaService(mySQLDbConfig); + } + + @Override + public Optional getResources(MySQLSchema resource) { + return schemaService.getSchema(resource.getMetadata().getName()); + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java index 168cd8db15..92ddb67a63 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaStatus.java @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.sample; -public class SchemaStatus { +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class SchemaStatus extends ObservedGenerationAwareStatus { private String url; diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java new file mode 100644 index 0000000000..836951a004 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.sample.schema; + +import java.io.Serializable; +import java.util.Objects; + +public class Schema implements Serializable { + + private String name; + private String characterSet; + + public Schema(String name, String characterSet) { + this.name = name; + this.characterSet = characterSet; + } + + public String getName() { + return name; + } + + public String getCharacterSet() { + return characterSet; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Schema schema = (Schema) o; + return Objects.equals(name, schema.name) && Objects.equals(characterSet, schema.characterSet); + } + + @Override + public int hashCode() { + return Objects.hash(name, characterSet); + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java new file mode 100644 index 0000000000..8c6cd31b70 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java @@ -0,0 +1,122 @@ +package io.javaoperatorsdk.operator.sample.schema; + +import java.sql.*; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.javaoperatorsdk.operator.sample.MySQLDbConfig; + +import static java.lang.String.format; + +public class SchemaService { + + + private static final Logger log = LoggerFactory.getLogger(SchemaService.class); + + private final MySQLDbConfig mySQLDbConfig; + + public SchemaService(MySQLDbConfig mySQLDbConfig) { + this.mySQLDbConfig = mySQLDbConfig; + } + + public Optional getSchema(String name) { + try (Connection connection = getConnection()) { + return getSchema(connection, name); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + public static void createSchemaAndRelatedUser(Connection connection, String schemaName, + String encoding, + String userName, + String password) { + try { + try (Statement statement = connection.createStatement()) { + statement.execute( + format( + "CREATE SCHEMA `%1$s` DEFAULT CHARACTER SET %2$s", + schemaName, encoding)); + } + if (!userExists(connection, userName)) { + try (Statement statement = connection.createStatement()) { + statement.execute(format("CREATE USER '%1$s' IDENTIFIED BY '%2$s'", userName, password)); + } + } + try (Statement statement = connection.createStatement()) { + statement.execute( + format("GRANT ALL ON `%1$s`.* TO '%2$s'", schemaName, userName)); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + public static void deleteSchemaAndRelatedUser(Connection connection, String schemaName, + String userName) { + try { + try (Statement statement = connection.createStatement()) { + statement.execute(format("DROP DATABASE `%1$s`", schemaName)); + } + log.info("Deleted Schema '{}'", schemaName); + if (userName != null) { + try (Statement statement = connection.createStatement()) { + statement.execute(format("DROP USER '%1$s'", userName)); + } + log.info("Deleted User '{}'", userName); + } + + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + private static boolean userExists(Connection connection, String username) { + try (PreparedStatement ps = + connection.prepareStatement( + "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = ?)")) { + ps.setString(1, username); + try (ResultSet resultSet = ps.executeQuery()) { + return resultSet.next(); + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + public static Optional getSchema(Connection connection, String schemaName) { + try (PreparedStatement ps = + connection.prepareStatement( + "SELECT * FROM information_schema.schemata WHERE schema_name = ?")) { + ps.setString(1, schemaName); + try (ResultSet resultSet = ps.executeQuery()) { + // CATALOG_NAME, SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME, SQL_PATH + var exists = resultSet.next(); + if (!exists) { + return Optional.empty(); + } else { + return Optional.of(new Schema(resultSet.getString("SCHEMA_NAME"), + resultSet.getString("DEFAULT_CHARACTER_SET_NAME"))); + } + } + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + + private Connection getConnection() { + try { + String connectionString = + format("jdbc:mysql://%1$s:%2$s", mySQLDbConfig.getHost(), mySQLDbConfig.getPort()); + + log.debug("Connecting to '{}' with user '{}'", connectionString, mySQLDbConfig.getUser()); + return DriverManager.getConnection(connectionString, mySQLDbConfig.getUser(), + mySQLDbConfig.getPassword()); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index ce3803534a..1d2c8d187b 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -18,7 +18,7 @@ import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.source.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; import static java.util.Collections.EMPTY_SET; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 115ba9f648..2c71dd9815 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -26,7 +26,7 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; -import io.javaoperatorsdk.operator.processing.event.source.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import okhttp3.Response; From 4611134a6c943a9c6038cc9660fec094751f1a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 16 Dec 2021 16:18:26 +0100 Subject: [PATCH 0184/1608] Internals docs (#759) --- docs/_data/sidebar.yml | 2 + docs/assets/images/architecture.svg | 4 ++ .../architecture-and-internals.md | 54 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 docs/assets/images/architecture.svg create mode 100644 docs/documentation/architecture-and-internals.md diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 4963e97be1..5bd1f2ce22 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -11,6 +11,8 @@ url: /docs/patterns-best-practices - title: FAQ url: /docs/faq + - title: Architecture and Internals + url: /docs/architecture-and-internals - title: Contributing url: /docs/contributing - title: Migrating from v1 to v2 diff --git a/docs/assets/images/architecture.svg b/docs/assets/images/architecture.svg new file mode 100644 index 0000000000..0f5a97da0d --- /dev/null +++ b/docs/assets/images/architecture.svg @@ -0,0 +1,4 @@ + + + +
Operator
Operator
Controller
Controller
EventSourceManager
EventSourceManager
EventProcessor
EventProcessor
ReconcilerDispatcher
ReconcilerDispatcher
EventSource
EventSource
Propagate Event
Propagate Event
0..*
0..*
1..*
1..*
ControllerResourceEventSource
ControllerResourceEventSource
Propagate Event
Propagate Event
Reconciler
Reconciler
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/documentation/architecture-and-internals.md b/docs/documentation/architecture-and-internals.md new file mode 100644 index 0000000000..7163824a3d --- /dev/null +++ b/docs/documentation/architecture-and-internals.md @@ -0,0 +1,54 @@ +--- +title: Architecture and Internals +description: Architecture and Internals for Developers +layout: docs +permalink: /docs/architecture-and-internals +--- + +# Architecture and Internals + +This document gives an overview of the internal structure and components of Java Operator SDK core, in order to make it +easier for developers to understand and contribute to it. However, this is just an extract of the backbone of the core +module, but other parts should be fairly easy to understand. We will maintain this document on developer feedback. + +## The Big Picture and Core Components + +![Alt text for broken image link](../assets/images/architecture.svg) + +[Operator](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java) +is a set of +independent [controllers](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java) +. Controller however, is an internal class managed by the framework itself. It encapsulates directly or indirectly all +the processing units for a single custom resource. Other components: + +- [EventSourceManager](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java) + aggregates all the event sources regarding a controller. Provides starts and stops the event sources. +- [ControllerResourceEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java) + is a central event source that watches the controller related custom resource for changes, propagates events and + caches the state of the custom resources. In the background from V2 it uses Informers. +- [EventProcessor](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java) + processes the incoming events. Implements execution serialization. Manages the executor service for execution. Also + implements the post-processing of after the reconciler was executed, like re-schedules and retries of events. +- [ReconcilerDispatcher](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java) + is responsible for managing logic around reconciler execution, deciding which method should be called of the + reconciler, managing the result + (UpdateControl and DeleteControl), making the instructed Kubernetes API calls. +- [Reconciler](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) + is the primary entry-point for the developers of the framework to implement the reconciliation logic. + +## Typical Workflow + +A typical workflows looks like following: + +1. An EventSource produces and event, that is propagated to the event processor. +2. In the event processor the related `CustomResource` is read from the cache based on the `ResourceID` in the event. +3. If there is no other execution running for the custom resource, an execution is submitted for the executor (thread + pool) . +4. Executor call EventDispatcher what decides which method to execute of the reconciler. Let's say in this case it + was `reconcile(...)` +5. After reconciler execution the Dispatcher calls Kubernetes API server, since the `reconcile` method returned + with `UpdateControl.updateStatus(...)` result. +6. Now the dispatcher finishes the execution and calls back `EventProcessor` to finalize the execution. +7. EventProcessor checks if there is no `reschedule` or `retry` required and if there are no subsequent events received + for the custom resource +8. Neither of this happened, therefore the event execution finished. From 22f03fef553cdd8ae2b4bb67c2041b0b6f669a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 17 Dec 2021 08:56:00 +0100 Subject: [PATCH 0185/1608] fix: resource cache interface for InformerEventSource (#758) --- operator-framework-core/pom.xml | 15 ----------- .../event/source/CachingEventSource.java | 21 ++++++---------- .../inbound/CachingInboundEventSource.java | 6 ----- .../source/informer/InformerEventSource.java | 25 ++++++++++++++++++- .../PerResourcePollingEventSource.java | 9 +++---- .../source/polling/PollingEventSource.java | 10 +++----- .../event/source/CachingEventSourceTest.java | 18 +------------ .../PerResourcePollingEventSourceTest.java | 17 ++----------- .../polling/PollingEventSourceTest.java | 14 +---------- pom.xml | 17 ------------- sample-operators/mysql-schema/pom.xml | 13 ---------- .../sample/MySQLSchemaReconciler.java | 14 +---------- 12 files changed, 42 insertions(+), 137 deletions(-) diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index eafb9ec1b1..47fc3f9881 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -105,20 +105,5 @@ awaitility test - - javax.cache - cache-api - ${jcache.version} - - - com.github.ben-manes.caffeine - caffeine - test - - - com.github.ben-manes.caffeine - jcache - test - diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index 9a2be41a70..cf0e709253 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -1,11 +1,9 @@ package io.javaoperatorsdk.operator.processing.event.source; +import java.util.Collections; +import java.util.Map; import java.util.Optional; - -import javax.cache.Cache; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.concurrent.ConcurrentHashMap; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.Event; @@ -25,13 +23,9 @@ */ public abstract class CachingEventSource extends LifecycleAwareEventSource { - private static final Logger log = LoggerFactory.getLogger(CachingEventSource.class); + protected Map cache = new ConcurrentHashMap<>(); - protected Cache cache; - - public CachingEventSource(Cache cache) { - this.cache = cache; - } + public CachingEventSource() {} protected void handleDelete(ResourceID relatedResourceID) { if (!isRunning()) { @@ -56,8 +50,8 @@ protected void handleEvent(T value, ResourceID relatedResourceID) { } } - public Cache getCache() { - return cache; + public Map getCache() { + return Collections.unmodifiableMap(cache); } public Optional getCachedValue(ResourceID resourceID) { @@ -67,6 +61,5 @@ public Optional getCachedValue(ResourceID resourceID) { @Override public void stop() throws OperatorException { super.stop(); - cache.close(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java index 51d1ed287d..308b4a36de 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java @@ -1,16 +1,10 @@ package io.javaoperatorsdk.operator.processing.event.source.inbound; -import javax.cache.Cache; - import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; public class CachingInboundEventSource extends CachingEventSource { - public CachingInboundEventSource(Cache cache) { - super(cache); - } - public void handleResourceEvent(T resource, ResourceID relatedResourceID) { super.handleEvent(resource, relatedResourceID); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 8095289221..07d1e20f54 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -1,8 +1,11 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,8 +19,10 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; -public class InformerEventSource extends AbstractEventSource { +public class InformerEventSource extends AbstractEventSource + implements ResourceCache { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); @@ -132,4 +137,22 @@ public T getAssociated(HasMetadata resource) { public SharedInformer getSharedInformer() { return sharedInformer; } + + @Override + public Optional get(ResourceID resourceID) { + return Optional.ofNullable(sharedInformer.getStore() + .getByKey(Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), + resourceID.getName()))); + } + + @Override + public Stream list(Predicate predicate) { + return sharedInformer.getStore().list().stream().filter(predicate); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + return sharedInformer.getStore().list().stream() + .filter(v -> namespace.equals(v.getMetadata().getNamespace()) && predicate.test(v)); + } } 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 bf9b41cf0e..61b76a5863 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 @@ -7,8 +7,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; -import javax.cache.Cache; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,14 +42,13 @@ public class PerResourcePollingEventSource private final long period; public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - ResourceCache resourceCache, long period, Cache cache) { - this(resourceSupplier, resourceCache, period, cache, null); + ResourceCache resourceCache, long period) { + this(resourceSupplier, resourceCache, period, null); } public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - ResourceCache resourceCache, long period, Cache cache, + ResourceCache resourceCache, long period, Predicate registerPredicate) { - super(cache); this.resourceSupplier = resourceSupplier; this.resourceCache = resourceCache; this.period = period; 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 b2c3fdff78..c979d35558 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 @@ -2,9 +2,6 @@ import java.util.*; import java.util.function.Supplier; -import java.util.stream.StreamSupport; - -import javax.cache.Cache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,8 +19,7 @@ public class PollingEventSource extends CachingEventSource { private final long period; public PollingEventSource(Supplier> supplier, - long period, Cache cache) { - super(cache); + long period) { this.supplierToPoll = supplier; this.period = period; } @@ -46,8 +42,8 @@ public void run() { protected void getStateAndFillCache() { var values = supplierToPoll.get(); values.forEach((k, v) -> super.handleEvent(v, k)); - StreamSupport.stream(cache.spliterator(), false) - .filter(e -> !values.containsKey(e.getKey())).map(Cache.Entry::getKey) + cache.keySet().stream() + .filter(e -> !values.containsKey(e)) .forEach(super::handleDelete); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java index 15fcca7253..7c264aa4ad 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java @@ -1,19 +1,11 @@ package io.javaoperatorsdk.operator.processing.event.source; -import javax.cache.Cache; -import javax.cache.CacheManager; -import javax.cache.configuration.MutableConfiguration; -import javax.cache.spi.CachingProvider; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.processing.event.ResourceID; - -import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; import static org.assertj.core.api.Assertions.assertThat; @@ -22,16 +14,11 @@ class CachingEventSourceTest { private CachingEventSource cachingEventSource; - private Cache cache; private EventHandler eventHandlerMock = mock(EventHandler.class); @BeforeEach public void setup() { - CachingProvider cachingProvider = new CaffeineCachingProvider(); - CacheManager cacheManager = cachingProvider.getCacheManager(); - cache = cacheManager.createCache("test-caching", new MutableConfiguration<>()); - - cachingEventSource = new SimpleCachingEventSource(cache); + cachingEventSource = new SimpleCachingEventSource(); cachingEventSource.setEventHandler(eventHandlerMock); cachingEventSource.start(); } @@ -89,9 +76,6 @@ public void noEventOnDeleteIfResourceWasNotInCacheBefore() { public static class SimpleCachingEventSource extends CachingEventSource { - public SimpleCachingEventSource(Cache cache) { - super(cache); - } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java index 6eb4b98e3b..ddf1d505fd 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java @@ -2,11 +2,6 @@ import java.util.Optional; -import javax.cache.Cache; -import javax.cache.CacheManager; -import javax.cache.configuration.MutableConfiguration; -import javax.cache.spi.CachingProvider; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -17,10 +12,7 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; - import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -32,22 +24,17 @@ class PerResourcePollingEventSourceTest { private PerResourcePollingEventSource.ResourceSupplier supplier = mock(PerResourcePollingEventSource.ResourceSupplier.class); private ResourceCache resourceCache = mock(ResourceCache.class); - private Cache cache; private EventHandler eventHandler = mock(EventHandler.class); private TestCustomResource testCustomResource = TestUtils.testCustomResource(); @BeforeEach public void setup() { - CachingProvider cachingProvider = new CaffeineCachingProvider(); - CacheManager cacheManager = cachingProvider.getCacheManager(); - cache = cacheManager.createCache("test-caching", new MutableConfiguration<>()); - when(resourceCache.get(any())).thenReturn(Optional.of(testCustomResource)); when(supplier.getResources(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())); pollingEventSource = - new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, cache); + new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD); pollingEventSource.setEventHandler(eventHandler); } @@ -63,7 +50,7 @@ public void pollsTheResourceAfterAwareOfIt() throws InterruptedException { @Test public void registeringTaskOnAPredicate() throws InterruptedException { - pollingEventSource = new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, cache, + pollingEventSource = new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, testCustomResource -> testCustomResource.getMetadata().getGeneration() > 1); pollingEventSource.setEventHandler(eventHandler); pollingEventSource.start(); 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 177760f94e..3a0c39243e 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 @@ -4,11 +4,6 @@ import java.util.Map; import java.util.function.Supplier; -import javax.cache.Cache; -import javax.cache.CacheManager; -import javax.cache.configuration.MutableConfiguration; -import javax.cache.spi.CachingProvider; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -17,8 +12,6 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; -import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; - import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; import static org.mockito.Mockito.*; @@ -26,16 +19,11 @@ class PollingEventSourceTest { private PollingEventSource pollingEventSource; private Supplier> supplier = mock(Supplier.class); - private Cache cache; private EventHandler eventHandler = mock(EventHandler.class); @BeforeEach public void setup() { - CachingProvider cachingProvider = new CaffeineCachingProvider(); - CacheManager cacheManager = cachingProvider.getCacheManager(); - cache = cacheManager.createCache("test-caching", new MutableConfiguration<>()); - - pollingEventSource = new PollingEventSource<>(supplier, 50, cache); + pollingEventSource = new PollingEventSource<>(supplier, 50); pollingEventSource.setEventHandler(eventHandler); } diff --git a/pom.xml b/pom.xml index 4248fca4ce..b708b6b6be 100644 --- a/pom.xml +++ b/pom.xml @@ -70,8 +70,6 @@ 2.17.1 1.0 1.6.2 - 1.1.1 - 3.0.4 @@ -170,21 +168,6 @@ operator-framework ${project.version} - - javax.cache - cache-api - ${jcache.version} - - - com.github.ben-manes.caffeine - caffeine - ${caffein.version} - - - com.github.ben-manes.caffeine - jcache - ${caffein.version} - diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index f5df325685..8693cf6154 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -67,19 +67,6 @@ jackson-dataformat-yaml 2.13.0 - - javax.cache - cache-api - ${jcache.version} - - - com.github.ben-manes.caffeine - caffeine - - - com.github.ben-manes.caffeine - jcache - diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 33b8df52df..7ecd883c01 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -6,11 +6,6 @@ import java.util.Base64; import java.util.Optional; -import javax.cache.Cache; -import javax.cache.CacheManager; -import javax.cache.configuration.MutableConfiguration; -import javax.cache.spi.CachingProvider; - import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,8 +21,6 @@ import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; -import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider; - import static java.lang.String.format; @ControllerConfiguration @@ -50,15 +43,10 @@ public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig my @Override public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { - CachingProvider cachingProvider = new CaffeineCachingProvider(); - CacheManager cacheManager = cachingProvider.getCacheManager(); - Cache schemaCache = - cacheManager.createCache("schema-cache", new MutableConfiguration<>()); perResourcePollingEventSource = new PerResourcePollingEventSource<>(new SchemaPollingResourceSupplier(mysqlDbConfig), - eventSourceRegistry.getControllerResourceEventSource().getResourceCache(), POLL_PERIOD, - schemaCache); + eventSourceRegistry.getControllerResourceEventSource().getResourceCache(), POLL_PERIOD); eventSourceRegistry.registerEventSource(perResourcePollingEventSource); } From 7b7b4e5e0d4d3b94ba7efae4d76b4d8e3f6436d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Dec 2021 08:56:25 +0100 Subject: [PATCH 0186/1608] chore(deps-dev): bump mockito-core from 4.1.0 to 4.2.0 (#761) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.1.0...v4.2.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b708b6b6be..24072140ef 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 5.10.1 1.7.32 2.16.0 - 4.1.0 + 4.2.0 3.12.0 1.0.1 0.19 From aa13f40dd7d0b60b0245920f0c33718e9157eb0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 09:27:50 +0100 Subject: [PATCH 0187/1608] chore(deps): bump jackson-dataformat-yaml from 2.13.0 to 2.13.1 (#764) Bumps [jackson-dataformat-yaml](https://github.com/FasterXML/jackson-dataformats-text) from 2.13.0 to 2.13.1. - [Release notes](https://github.com/FasterXML/jackson-dataformats-text/releases) - [Commits](https://github.com/FasterXML/jackson-dataformats-text/compare/jackson-dataformats-text-2.13.0...jackson-dataformats-text-2.13.1) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sample-operators/mysql-schema/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 8693cf6154..e3fd67c001 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -65,7 +65,7 @@ com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.13.0 + 2.13.1 From 94838a7e941fbd1ee0009fec70312608f9575da6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Dec 2021 09:45:58 +0100 Subject: [PATCH 0188/1608] chore(deps): bump log4j-core from 2.16.0 to 2.17.0 (#763) Bumps log4j-core from 2.16.0 to 2.17.0. --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-core dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 24072140ef..5798a1d054 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 5.8.2 5.10.1 1.7.32 - 2.16.0 + 2.17.0 4.2.0 3.12.0 1.0.1 From 319a76e0410165a512c06456844e6b8c805c43c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 20 Dec 2021 14:40:42 +0100 Subject: [PATCH 0189/1608] fix: fabric8 version bump (#768) --- pom.xml | 2 +- .../javaoperatorsdk/operator/sample/WebappReconciler.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 5798a1d054..eb4ba03e66 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ ${java.version} 5.8.2 - 5.10.1 + 5.11.0 1.7.32 2.17.0 4.2.0 diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 2c71dd9815..d06740c4b4 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -187,13 +187,13 @@ public SimpleListener(CompletableFuture data, ByteArrayOutputStream baos } @Override - public void onOpen(Response response) { - log.debug("Reading data... " + response.message()); + public void onOpen() { + log.debug("Reading data... "); } @Override public void onFailure(Throwable t, Response response) { - log.debug(t.getMessage() + " " + response.message()); + log.debug(t.getMessage()); data.completeExceptionally(t); } From caa71ebfe79d134587de212d60b6ca8e364babfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 20 Dec 2021 16:54:32 +0100 Subject: [PATCH 0190/1608] fix: minor fixes (#767) --- .../operator/api/ObservedGenerationAware.java | 22 +++++----- .../api/reconciler/ErrorStatusHandler.java | 6 +-- .../operator/api/reconciler/Reconciler.java | 43 ++++++++----------- .../processing/event/EventProcessor.java | 4 -- .../event/ReconciliationDispatcher.java | 3 +- .../ControllerResourceEventSource.java | 3 -- .../processing/retry/RetryExecution.java | 5 +-- .../event/ReconciliationDispatcherTest.java | 7 +-- 8 files changed, 38 insertions(+), 55 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java index 370e497652..54b1321d52 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -6,16 +6,18 @@ /** * If the custom resource's status implements this interface, the observed generation will be - * automatically handled. The last observed generation will be updated on status when the status is - * instructed to be updated (see below). In addition to that, controller configuration will be - * checked if is set to generation aware. If generation aware config is turned off, this interface - * is ignored. - * - * In order to work the status object returned by CustomResource.getStatus() should not be null. In - * addition to that from the controller that the {@link UpdateControl#updateStatus(HasMetadata)} or - * {@link UpdateControl#updateResourceAndStatus(HasMetadata)} should be returned. The observed - * generation is not updated in other cases. - * + * automatically handled. The last observed generation will be updated on status. In addition to + * that, controller configuration will be checked if is set to be generation aware. If generation + * aware config is turned off, this interface is ignored. + *

+ * In order for this automatic handling to work the status object returned by + * {@link CustomResource#getStatus()} should not be null. + *

+ * The observed generation is updated even when {@link UpdateControl#noUpdate()} or + * {@link UpdateControl#updateResource(HasMetadata)} is called. Although those results call normally + * does not result in a status update, there will be a subsequent status update Kubernetes API call + * in this case. + * * @see ObservedGenerationAwareStatus */ public interface ObservedGenerationAware { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java index 60e9e671b2..7a48f6f661 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java @@ -8,9 +8,9 @@ public interface ErrorStatusHandler { /** *

- * Reconcile can implement this interface in order to update the status sub-resource in the case - * when the last reconciliation retry attempt is failed on the Reconciler. In that case the - * updateErrorStatus is called automatically. + * Reconciler can implement this interface in order to update the status sub-resource in the case + * an exception in thrown. In that case + * {@link #updateErrorStatus(HasMetadata, RetryInfo, RuntimeException)} is called automatically. *

* The result of the method call is used to make a status update on the custom resource. This is * always a sub-resource update request, so no update on custom resource itself (like spec of diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 7a6977d940..3e7356924b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -5,21 +5,30 @@ public interface Reconciler { /** - * Note that this method is used in combination of finalizers. If automatic finalizer handling is - * turned off for the controller, this method is not called. + * The implementation of this operation is required to be idempotent. Always use the UpdateControl + * object to make updates on custom resource if possible. + * + * @param resource the resource that has been created or updated + * @param context the context with which the operation is executed + * @return UpdateControl to manage updates on the custom resource (usually the status) after + * reconciliation. + */ + UpdateControl reconcile(R resource, Context context); + + /** + * Note that this method is used in combination with finalizers. If automatic finalizer handling + * is turned off for the controller, this method is not called. * - * The implementation should delete the associated component(s). Note that this is method is - * called when an object is marked for deletion. After it's executed the custom resource finalizer - * is automatically removed by the framework; unless the return value is + * The implementation should delete the associated component(s). This method is called when an + * object is marked for deletion. After it's executed the custom resource finalizer is + * automatically removed by the framework; unless the return value is * {@link DeleteControl#noFinalizerRemoval()}, which indicates that the controller has determined * that the resource should not be deleted yet. This is usually a corner case, when a cleanup is * tried again eventually. * *

- * It's important that this method be idempotent, as it could be called several times, depending - * on the conditions and the controller's configuration (for example, if the controller is - * configured to not use a finalizer but the resource does have finalizers, it might be be - * "offered" again for deletion several times until the finalizers are all removed. + * It's important for implementations of this method to be idempotent, since it can be called + * several times. * * @param resource the resource that is marked for deletion * @param context the context with which the operation is executed @@ -32,20 +41,4 @@ default DeleteControl cleanup(R resource, Context context) { return DeleteControl.defaultDelete(); } - /** - * The implementation of this operation is required to be idempotent. Always use the UpdateControl - * object to make updates on custom resource if possible. Also always use the custom resource - * parameter (not the custom resource that might be in the events) - * - * @param resource the resource that has been created or updated - * @param context the context with which the operation is executed - * @return The resource is updated in api server if the return value is not - * {@link UpdateControl#noUpdate()}. This the common use cases. However in cases, for - * example the operator is restarted, and we don't want to have an update call to k8s api - * to be made unnecessarily, by returning {@link UpdateControl#noUpdate()} this update can - * be skipped. However we will always call an update if there is no finalizer on object - * and it's not marked for deletion. - */ - UpdateControl reconcile(R resource, Context context); - } 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 1f0161aeef..32d43ac3a0 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 @@ -31,10 +31,6 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; -/** - * Event handler that makes sure that events are processed in a "single threaded" way per resource - * UID, while buffering events which are received during an execution. - */ class EventProcessor implements EventHandler, LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventProcessor.class); 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 314698fd71..9e7f036e39 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 @@ -87,7 +87,8 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) private boolean shouldNotDispatchToDelete(R resource) { // we don't dispatch to delete if the controller is configured to use a finalizer but that // finalizer is not present (which means it's already been removed) - return configuration().useFinalizer() && !resource.hasFinalizer(configuration().getFinalizer()); + return !configuration().useFinalizer() || (configuration().useFinalizer() + && !resource.hasFinalizer(configuration().getFinalizer())); } private PostExecutionControl handleReconcile( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 2386cd4dbc..a8859c8d43 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -26,9 +26,6 @@ import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; -/** - * This is a special case since is not bound to a single custom resource - */ public class ControllerResourceEventSource extends AbstractEventSource implements ResourceEventHandler { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java index 814a85b7d3..78f1b420c5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/RetryExecution.java @@ -7,10 +7,7 @@ public interface RetryExecution extends RetryInfo { /** - * Calculates the delay for the next execution. This method should return 0, when called first - * time; - * - * @return the time to wait until the next execution in millisecondsz + * @return the time to wait until the next execution in milliseconds */ Optional nextDelay(); } 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 4de29f596c..131da0fa95 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 @@ -139,17 +139,14 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { verify(reconciler, times(1)).cleanup(eq(testCustomResource), any()); } - /** - * Note that there could be more finalizers. Out of our control. - */ @Test - void callDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { + void doesNotCallDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { configureToNotUseFinalizer(); markForDeletion(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - verify(reconciler).cleanup(eq(testCustomResource), any()); + verify(reconciler, times(0)).cleanup(eq(testCustomResource), any()); } @Test From 27865f5cb772e65814fdae506cdeab2fc44c2303 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Dec 2021 10:05:12 +0100 Subject: [PATCH 0191/1608] chore(deps): bump spring-boot.version from 2.6.1 to 2.6.2 (#773) Bumps `spring-boot.version` from 2.6.1 to 2.6.2. Updates `spring-boot-dependencies` from 2.6.1 to 2.6.2 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.1...v2.6.2) Updates `spring-boot-maven-plugin` from 2.6.1 to 2.6.2 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.1...v2.6.2) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index eb4ba03e66..7e4dad678a 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 1.13.0 3.21.0 4.1.1 - 2.6.1 + 2.6.2 1.8.1 2.11 From 37ea3a6d2d0644ed8c2c136946ff2909a448284b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 22 Dec 2021 14:51:27 +0100 Subject: [PATCH 0192/1608] feat: improve Context / EventSource API - Record type handled by EventSource so that we can retrieve an associated secondary resource from a primary one directly from the Context - Introduce `ResourceEventSource` concept - Update sample reconcilers to use the new Context functionality - Make `EventSourceInitializer` return list of `EventSource`s This makes the intent clearer: the `EventSourceInitializer` prepares `EventSource`s that the SDK then registers automatically, without the reconciler having to call the register method in `prepareEventSources`. This also allows for removing the `EventSourceRegistry` interface altogether. - Extract interfaces for mappers instead of using `Function` - Introduce `EventSourceInitializationContext` for future-proofing --- .../operator/ReconcilerUtils.java | 3 +- .../api/config/ControllerConfiguration.java | 5 +- .../operator/api/reconciler/Constants.java | 10 + .../operator/api/reconciler/Context.java | 1 + .../reconciler/ControllerConfiguration.java | 14 +- .../api/reconciler/DefaultContext.java | 18 +- .../EventSourceInitializationContext.java | 17 ++ .../reconciler/EventSourceInitializer.java | 21 +- .../operator/api/reconciler/Reconciler.java | 1 - .../operator/processing/Controller.java | 11 +- .../processing/event/EventProcessor.java | 12 +- .../processing/event/EventSourceManager.java | 194 +++++++++++++++--- .../event/ReconciliationDispatcher.java | 3 +- .../event/source/AbstractEventSource.java | 25 ++- .../source/AbstractResourceEventSource.java | 18 ++ ...AssociatedSecondaryResourceIdentifier.java | 9 + .../ResourceCache.java => Cache.java} | 17 +- .../event/source/CachingEventSource.java | 82 +++++++- .../processing/event/source/EventSource.java | 6 +- .../event/source/EventSourceRegistry.java | 26 --- .../source/LifecycleAwareEventSource.java | 22 -- .../source/PrimaryResourcesRetriever.java | 11 + .../event/source/ResourceCache.java | 16 ++ .../event/source/ResourceEventSource.java | 10 + .../event/source/UpdatableCache.java | 9 + .../controller/ControllerResourceCache.java | 13 +- .../ControllerResourceEventSource.java | 14 +- .../inbound/CachingInboundEventSource.java | 7 +- .../inbound/SimpleInboundEventSource.java | 6 +- .../source/informer/InformerEventSource.java | 68 +++--- .../event/source/informer/Mappers.java | 28 ++- .../PerResourcePollingEventSource.java | 15 +- .../source/polling/PollingEventSource.java | 10 +- .../event/source/timer/TimerEventSource.java | 5 +- .../processing/retry/GenericRetry.java | 11 +- .../event/EventSourceManagerTest.java | 132 +++++++++++- .../event/source/AbstractEventSourceTest.java | 38 ++++ .../event/source/CachingEventSourceTest.java | 56 +++-- ...ceWhitelistEventFilterEventFilterTest.java | 9 +- .../event/source/ResourceEventFilterTest.java | 22 +- .../ControllerResourceEventSourceTest.java | 49 ++--- .../PerResourcePollingEventSourceTest.java | 45 ++-- .../polling/PollingEventSourceTest.java | 20 +- .../source/timer/TimerEventSourceTest.java | 50 +++-- .../ErrorStatusHandlerTestReconciler.java | 2 +- ...formerEventSourceTestCustomReconciler.java | 20 +- .../ObservedGenerationTestReconciler.java | 2 +- .../retry/RetryTestCustomReconciler.java | 4 +- .../sample/MySQLSchemaReconciler.java | 25 +-- .../operator/sample/TomcatReconciler.java | 31 ++- .../operator/sample/WebappReconciler.java | 24 +-- 51 files changed, 868 insertions(+), 399 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{controller/ResourceCache.java => Cache.java} (55%) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/UpdatableCache.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 487c923349..dbf3d1a59e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -2,6 +2,7 @@ import java.util.Locale; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -19,7 +20,7 @@ public static String getNameFor(Class reconcilerClass) { final var annotation = reconcilerClass.getAnnotation(ControllerConfiguration.class); if (annotation != null) { final var name = annotation.name(); - if (!ControllerConfiguration.EMPTY_STRING.equals(name)) { + if (!Constants.EMPTY_STRING.equals(name)) { return name; } } 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 31d701c03d..94d2e0a8f4 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 @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -67,7 +68,7 @@ static boolean currentNamespaceWatched(Set namespaces) { return namespaces != null && namespaces.size() == 1 && namespaces.contains( - io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.WATCH_CURRENT_NAMESPACE); + Constants.WATCH_CURRENT_NAMESPACE); } /** @@ -99,7 +100,7 @@ default RetryConfiguration getRetryConfiguration() { default void setConfigurationService(ConfigurationService service) {} default boolean useFinalizer() { - return !io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER + return !Constants.NO_FINALIZER .equals(getFinalizer()); } 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 new file mode 100644 index 0000000000..075b4e79a3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +public final class Constants { + + public static final String EMPTY_STRING = ""; + public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; + public static final String NO_FINALIZER = "JOSDK_NO_FINALIZER"; + + private Constants() {} +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index bc8966f31c..4c716c2842 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -6,4 +6,5 @@ public interface Context { Optional getRetryInfo(); + T getSecondaryResource(Class expectedType, String... qualifier); } 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 b5c6265ae1..f72354cf93 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 @@ -11,20 +11,16 @@ @Target({ElementType.TYPE}) public @interface ControllerConfiguration { - String EMPTY_STRING = ""; - String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; - String NO_FINALIZER = "JOSDK_NO_FINALIZER"; - - String name() default EMPTY_STRING; + String name() default Constants.EMPTY_STRING; /** * Optional finalizer name, if it is not provided, one will be automatically generated. If the - * provided value is the value specified by {@link #NO_FINALIZER}, then no finalizer will be added - * to custom resources. + * provided value is the value specified by {@link Constants#NO_FINALIZER}, then no finalizer will + * be added to custom resources. * * @return the finalizer name */ - String finalizerName() default EMPTY_STRING; + String finalizerName() default Constants.EMPTY_STRING; /** * If true, will dispatch new event to the controller if generation increased since the last @@ -50,7 +46,7 @@ * * @return the label selector */ - String labelSelector() default EMPTY_STRING; + String labelSelector() default Constants.EMPTY_STRING; /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 8f73af29e7..7aa856097e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -2,16 +2,30 @@ import java.util.Optional; -public class DefaultContext implements Context { +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.Controller; + +public class DefaultContext

implements Context { private final RetryInfo retryInfo; + private final Controller

controller; + private final P primaryResource; - public DefaultContext(RetryInfo retryInfo) { + public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; + this.controller = controller; + this.primaryResource = primaryResource; } @Override public Optional getRetryInfo() { return Optional.ofNullable(retryInfo); } + + @Override + public T getSecondaryResource(Class expectedType, String... qualifier) { + final var eventSource = + controller.getEventSourceManager().getResourceEventSourceFor(expectedType, qualifier); + return eventSource.map(es -> es.getAssociated(primaryResource)).orElse(null); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java new file mode 100644 index 0000000000..f2a053552c --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; + +public class EventSourceInitializationContext

{ + + private final ResourceCache

primaryCache; + + public EventSourceInitializationContext(ResourceCache

primaryCache) { + this.primaryCache = primaryCache; + } + + public ResourceCache

getPrimaryCache() { + return primaryCache; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 03eaf2b062..7c8d8e9f7c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -1,17 +1,24 @@ package io.javaoperatorsdk.operator.api.reconciler; +import java.util.List; + import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; -public interface EventSourceInitializer { +/** + * An interface that a {@link Reconciler} can implement to have the SDK register the provided + * {@link EventSource} + * + * @param

the primary resource type handled by the associated {@link Reconciler} + */ +public interface EventSourceInitializer

{ /** - * Reconciler can implement this interface typically to register event sources. But can access - * CustomResourceEventSource, what might be useful for some edge cases. + * Prepares a list of {@link EventSource} implementations to be registered by the SDK. * - * @param eventSourceRegistry the {@link EventSourceRegistry} where event sources can be - * registered. + * @param primaryCache a cache providing direct access to primary resources so that event sources + * can extract relevant information from primary resources as needed */ - void prepareEventSources(EventSourceRegistry eventSourceRegistry); + List prepareEventSources(EventSourceInitializationContext

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 3e7356924b..2b725afd73 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -40,5 +40,4 @@ public interface Reconciler { default DeleteControl cleanup(R resource, Context context) { return DeleteControl.defaultDelete(); } - } 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 c8c96cfd6a..ba5035c712 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 @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing; +import java.util.List; import java.util.Objects; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -15,11 +16,12 @@ import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializationContext; 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.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { @@ -96,7 +98,7 @@ public UpdateControl execute() { } @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + public List prepareEventSources(EventSourceInitializationContext context) { throw new UnsupportedOperationException("This method should never be called directly"); } @@ -169,7 +171,10 @@ public void start() throws OperatorException { eventSourceManager = new EventSourceManager<>(this); if (reconciler instanceof EventSourceInitializer) { - ((EventSourceInitializer) reconciler).prepareEventSources(eventSourceManager); + ((EventSourceInitializer) reconciler) + .prepareEventSources(new EventSourceInitializationContext<>( + eventSourceManager.getControllerResourceEventSource().getResourceCache())) + .forEach(eventSourceManager::registerEventSource); } if (failOnMissingCurrentNS()) { throw new OperatorException( 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 32d43ac3a0..894cef8298 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 @@ -20,8 +20,8 @@ import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.MDCUtils; +import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -44,7 +44,7 @@ class EventProcessor implements EventHandler, LifecycleAw private final ReentrantLock lock = new ReentrantLock(); private final Metrics metrics; private volatile boolean running; - private final ResourceCache resourceCache; + private final Cache cache; private final EventSourceManager eventSourceManager; private final EventMarker eventMarker = new EventMarker(); @@ -70,7 +70,7 @@ class EventProcessor implements EventHandler, LifecycleAw reconciliationDispatcher, retry, null, eventSourceManager); } - private EventProcessor(ResourceCache resourceCache, ExecutorService executor, + private EventProcessor(Cache cache, ExecutorService executor, String relatedControllerName, ReconciliationDispatcher reconciliationDispatcher, Retry retry, Metrics metrics, EventSourceManager eventSourceManager) { @@ -83,7 +83,7 @@ private EventProcessor(ResourceCache resourceCache, ExecutorService executor, this.controllerName = relatedControllerName; this.reconciliationDispatcher = reconciliationDispatcher; this.retry = retry; - this.resourceCache = resourceCache; + this.cache = cache; this.metrics = metrics != null ? metrics : Metrics.NOOP; this.eventSourceManager = eventSourceManager; } @@ -120,7 +120,7 @@ public void handleEvent(Event event) { private void submitReconciliationExecution(ResourceID resourceID) { try { boolean controllerUnderExecution = isControllerUnderExecution(resourceID); - Optional latest = resourceCache.get(resourceID); + Optional latest = cache.get(resourceID); latest.ifPresent(MDCUtils::addResourceInfo); if (!controllerUnderExecution && latest.isPresent()) { setUnderExecutionProcessing(resourceID); @@ -216,7 +216,7 @@ private boolean isCacheReadyForInstantReconciliation(ExecutionScope execution .getUpdatedCustomResource() .orElseThrow(() -> new IllegalStateException( "Updated custom resource must be present at this point of time"))); - String cachedCustomResourceVersion = getVersion(resourceCache + String cachedCustomResourceVersion = getVersion(cache .get(executionScope.getCustomResourceID()) .orElseThrow(() -> new IllegalStateException( "Cached custom resource must be present at this point"))); 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 6848fcbb37..fb62b8b22c 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,7 +1,17 @@ package io.javaoperatorsdk.operator.processing.event; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,44 +22,34 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; -public class EventSourceManager - implements EventSourceRegistry, LifecycleAware { +public class EventSourceManager implements LifecycleAware { private static final Logger log = LoggerFactory.getLogger(EventSourceManager.class); private final ReentrantLock lock = new ReentrantLock(); - // This needs to be a list since the event source must be started in a deterministic order. The - // controllerResourceEventSource must be always the first to have informers available for other - // informers to access the main controller cache. - private final List eventSources = Collections.synchronizedList(new ArrayList<>()); + private final EventSources eventSources = new EventSources<>(); private final EventProcessor eventProcessor; - private TimerEventSource retryAndRescheduleTimerEventSource; - private ControllerResourceEventSource controllerResourceEventSource; private final Controller controller; EventSourceManager(EventProcessor eventProcessor) { this.eventProcessor = eventProcessor; controller = null; - initRetryEventSource(); + registerEventSource(eventSources.initRetryEventSource()); } public EventSourceManager(Controller controller) { this.controller = controller; - controllerResourceEventSource = new ControllerResourceEventSource<>(controller); + // controller event source needs to be available before we create the event processor + final var controllerEventSource = eventSources.initControllerEventSource(controller); this.eventProcessor = new EventProcessor<>(this); - registerEventSource(controllerResourceEventSource); - initRetryEventSource(); - } - - private void initRetryEventSource() { - retryAndRescheduleTimerEventSource = new TimerEventSource<>(); - registerEventSource(retryAndRescheduleTimerEventSource); + registerEventSource(eventSources.initRetryEventSource()); + registerEventSource(controllerEventSource); } @Override @@ -89,7 +89,6 @@ public void stop() { eventProcessor.stop(); } - @Override public final void registerEventSource(EventSource eventSource) throws OperatorException { Objects.requireNonNull(eventSource, "EventSource must not be null"); @@ -110,7 +109,7 @@ public final void registerEventSource(EventSource eventSource) } public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldResource) { - for (EventSource eventSource : this.eventSources) { + for (var eventSource : eventSources) { if (eventSource instanceof ResourceEventAware) { var lifecycleAwareES = ((ResourceEventAware) eventSource); switch (action) { @@ -128,21 +127,162 @@ public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldRes } } - @Override - public Set getRegisteredEventSources() { - return new HashSet<>(eventSources); + EventHandler getEventHandler() { + return eventProcessor; + } + + Set getRegisteredEventSources() { + return eventSources.all(); } - @Override public ControllerResourceEventSource getControllerResourceEventSource() { - return controllerResourceEventSource; + return eventSources.controllerResourceEventSource; + } + + public Optional> getResourceEventSourceFor( + Class dependentType, String... qualifier) { + if (dependentType == null) { + return Optional.empty(); + } + String name = (qualifier != null && qualifier.length >= 1) ? qualifier[0] : ""; + final var eventSource = eventSources.get(dependentType, name); + return Optional.ofNullable(eventSource); } TimerEventSource retryEventSource() { - return retryAndRescheduleTimerEventSource; + return eventSources.retryAndRescheduleTimerEventSource; } Controller getController() { return controller; } + + private static class EventSources implements Iterable { + private final ConcurrentNavigableMap> sources = + new ConcurrentSkipListMap<>(); + private final TimerEventSource retryAndRescheduleTimerEventSource = new TimerEventSource<>(); + private ControllerResourceEventSource controllerResourceEventSource; + + + ControllerResourceEventSource initControllerEventSource(Controller controller) { + controllerResourceEventSource = new ControllerResourceEventSource<>(controller); + return controllerResourceEventSource; + } + + TimerEventSource initRetryEventSource() { + return retryAndRescheduleTimerEventSource; + } + + @Override + public Iterator iterator() { + return sources.values().stream().flatMap(Collection::stream).iterator(); + } + + public Set all() { + return new LinkedHashSet<>(sources.values().stream().flatMap(Collection::stream) + .collect(Collectors.toList())); + } + + public void clear() { + sources.clear(); + } + + public boolean contains(EventSource source) { + final var eventSources = sources.get(keyFor(source)); + if (eventSources == null || eventSources.isEmpty()) { + return false; + } + return findMatchingSource(qualifier(source), eventSources).isPresent(); + } + + public void add(EventSource eventSource) { + if (contains(eventSource)) { + throw new IllegalArgumentException("An event source is already registered for the " + + keyAsString(getDependentType(eventSource), qualifier(eventSource)) + + " class/name combination"); + } + sources.computeIfAbsent(keyFor(eventSource), k -> new ArrayList<>()).add(eventSource); + } + + private Class getDependentType(EventSource source) { + return source instanceof ResourceEventSource + ? ((ResourceEventSource) source).getResourceClass() + : source.getClass(); + } + + private String qualifier(EventSource source) { + return source.name(); + } + + private String keyFor(EventSource source) { + return keyFor(getDependentType(source)); + } + + private String keyFor(Class dependentType) { + var key = dependentType.getCanonicalName(); + + // make sure timer event source is started first, then controller event source + // this is needed so that these sources are set when informer sources start so that events can + // properly be processed + if (controllerResourceEventSource != null + && key.equals(controllerResourceEventSource.getResourceClass().getCanonicalName())) { + key = 1 + "-" + key; + } else if (key.equals(retryAndRescheduleTimerEventSource.getClass().getCanonicalName())) { + key = 0 + "-" + key; + } + return key; + } + + public ResourceEventSource get(Class dependentType, String qualifier) { + final var sourcesForType = sources.get(keyFor(dependentType)); + if (sourcesForType == null || sourcesForType.isEmpty()) { + return null; + } + + final var size = sourcesForType.size(); + final EventSource source; + if (size == 1) { + source = sourcesForType.get(0); + } else { + if (qualifier == null || qualifier.isBlank()) { + throw new IllegalArgumentException("There are multiple EventSources registered for type " + + dependentType.getCanonicalName() + + ", you need to provide a qualifier to specify which EventSource you want to query. Known qualifiers: " + + sourcesForType.stream().map(this::qualifier).collect(Collectors.joining(","))); + } + source = findMatchingSource(qualifier, sourcesForType).orElse(null); + + if (source == null) { + return null; + } + } + + if (!(source instanceof ResourceEventSource)) { + throw new IllegalArgumentException(source + " associated with " + + keyAsString(dependentType, qualifier) + " is not a " + + ResourceEventSource.class.getSimpleName()); + } + final var res = (ResourceEventSource) source; + final var resourceClass = res.getResourceClass(); + if (!resourceClass.isAssignableFrom(dependentType)) { + throw new IllegalArgumentException(source + " associated with " + + keyAsString(dependentType, qualifier) + + " is handling " + resourceClass.getName() + " resources but asked for " + + dependentType.getName()); + } + return res; + } + + private Optional findMatchingSource(String qualifier, + List sourcesForType) { + return sourcesForType.stream().filter(es -> qualifier(es).equals(qualifier)).findAny(); + } + + @SuppressWarnings("rawtypes") + private String keyAsString(Class dependentType, String qualifier) { + return qualifier != null && qualifier.length() > 0 + ? "(" + dependentType.getName() + ", " + qualifier + ")" + : dependentType.getName(); + } + } } 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 9e7f036e39..310efaa9dc 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 @@ -67,8 +67,7 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) return PostExecutionControl.defaultDispatch(); } - Context context = - new DefaultContext(executionScope.getRetryInfo()); + Context context = new DefaultContext<>(executionScope.getRetryInfo(), controller, resource); if (markedForDeletion) { return handleCleanup(resource, context); } else { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java index ddc787ad2d..5fa45e0a25 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java @@ -1,14 +1,33 @@ package io.javaoperatorsdk.operator.processing.event.source; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.EventHandler; public abstract class AbstractEventSource implements EventSource { - protected volatile EventHandler eventHandler; + private volatile EventHandler handler; + private volatile boolean running = false; + + protected EventHandler getEventHandler() { + return handler; + } @Override - public void setEventHandler(EventHandler eventHandler) { - this.eventHandler = eventHandler; + public void setEventHandler(EventHandler handler) { + this.handler = handler; } + public boolean isRunning() { + return running; + } + + @Override + public void start() throws OperatorException { + running = true; + } + + @Override + public void stop() throws OperatorException { + running = false; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java new file mode 100644 index 0000000000..de3340eff5 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractResourceEventSource.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public abstract class AbstractResourceEventSource

+ extends AbstractEventSource + implements ResourceEventSource { + private final Class resourceClass; + + protected AbstractResourceEventSource(Class resourceClass) { + this.resourceClass = resourceClass; + } + + @Override + public Class getResourceClass() { + return resourceClass; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java new file mode 100644 index 0000000000..b402429baa --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@FunctionalInterface +public interface AssociatedSecondaryResourceIdentifier

{ + ResourceID associatedSecondaryID(P primary); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Cache.java similarity index 55% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceCache.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Cache.java index 5508dd43eb..200faecbd0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/Cache.java @@ -1,27 +1,26 @@ -package io.javaoperatorsdk.operator.processing.event.source.controller; +package io.javaoperatorsdk.operator.processing.event.source; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; -import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; @SuppressWarnings({"rawtypes", "unchecked"}) -public interface ResourceCache { +public interface Cache { Predicate TRUE = (a) -> true; Optional get(ResourceID resourceID); - default Stream list() { - return list(TRUE); + default boolean contains(ResourceID resourceID) { + return get(resourceID).isPresent(); } - Stream list(Predicate predicate); + Stream keys(); - default Stream list(String namespace) { - return list(namespace, TRUE); + default Stream list() { + return list(TRUE); } - Stream list(String namespace, Predicate predicate); + Stream list(Predicate predicate); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index cf0e709253..5e65518282 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -1,10 +1,12 @@ package io.javaoperatorsdk.operator.processing.event.source; -import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Stream; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -21,11 +23,35 @@ * * @param represents the type of resources (usually external non-kubernetes ones) being handled. */ -public abstract class CachingEventSource extends LifecycleAwareEventSource { +public abstract class CachingEventSource + extends AbstractResourceEventSource implements Cache { - protected Map cache = new ConcurrentHashMap<>(); + protected UpdatableCache cache; - public CachingEventSource() {} + public CachingEventSource(Class resourceClass) { + super(resourceClass); + cache = initCache(); + } + + @Override + public Optional get(ResourceID resourceID) { + return cache.get(resourceID); + } + + @Override + public boolean contains(ResourceID resourceID) { + return cache.contains(resourceID); + } + + @Override + public Stream keys() { + return cache.keys(); + } + + @Override + public Stream list(Predicate predicate) { + return cache.list(predicate); + } protected void handleDelete(ResourceID relatedResourceID) { if (!isRunning()) { @@ -34,8 +60,8 @@ protected void handleDelete(ResourceID relatedResourceID) { var cachedValue = cache.get(relatedResourceID); cache.remove(relatedResourceID); // we only propagate event if the resource was previously in cache - if (cachedValue != null) { - eventHandler.handleEvent(new Event(relatedResourceID)); + if (cachedValue.isPresent()) { + getEventHandler().handleEvent(new Event(relatedResourceID)); } } @@ -44,22 +70,56 @@ protected void handleEvent(T value, ResourceID relatedResourceID) { return; } var cachedValue = cache.get(relatedResourceID); - if (cachedValue == null || !cachedValue.equals(value)) { + if (cachedValue.map(v -> !v.equals(value)).orElse(true)) { cache.put(relatedResourceID, value); - eventHandler.handleEvent(new Event(relatedResourceID)); + getEventHandler().handleEvent(new Event(relatedResourceID)); } } - public Map getCache() { - return Collections.unmodifiableMap(cache); + protected UpdatableCache initCache() { + return new MapCache<>(); } public Optional getCachedValue(ResourceID resourceID) { - return Optional.ofNullable(cache.get(resourceID)); + return cache.get(resourceID); } @Override public void stop() throws OperatorException { super.stop(); } + + @Override + public T getAssociated(P primary) { + return cache.get(ResourceID.fromResource(primary)).orElse(null); + } + + protected static class MapCache implements UpdatableCache { + private final Map cache = new ConcurrentHashMap<>(); + + @Override + public Optional get(ResourceID resourceID) { + return Optional.ofNullable(cache.get(resourceID)); + } + + @Override + public Stream keys() { + return cache.keySet().stream(); + } + + @Override + public Stream list(Predicate predicate) { + return cache.values().stream().filter(predicate); + } + + @Override + public T remove(ResourceID key) { + return cache.remove(key); + } + + @Override + public void put(ResourceID key, T resource) { + cache.put(key, resource); + } + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java index 18e47d03db..8d606fbd21 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 @@ -4,7 +4,9 @@ import io.javaoperatorsdk.operator.processing.event.EventHandler; public interface EventSource extends LifecycleAware { + default String name() { + return getClass().getCanonicalName(); + } - void setEventHandler(EventHandler eventHandler); - + void setEventHandler(EventHandler handler); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java deleted file mode 100644 index dca5436427..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -import java.util.Set; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; - -public interface EventSourceRegistry { - - /** - * Add the {@link EventSource} identified by the given name to the event manager. - * - * @param eventSource the {@link EventSource} to register - * @throws IllegalStateException if an {@link EventSource} with the same name is already - * registered. - * @throws OperatorException if an error occurred during the registration process - */ - void registerEventSource(EventSource eventSource) - throws IllegalStateException, OperatorException; - - Set getRegisteredEventSources(); - - ControllerResourceEventSource getControllerResourceEventSource(); - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java deleted file mode 100644 index 6b2d79fd1a..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/LifecycleAwareEventSource.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -import io.javaoperatorsdk.operator.OperatorException; - -public abstract class LifecycleAwareEventSource extends AbstractEventSource { - - private volatile boolean running = false; - - public boolean isRunning() { - return running; - } - - @Override - public void start() throws OperatorException { - running = true; - } - - @Override - public void stop() throws OperatorException { - running = false; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java new file mode 100644 index 0000000000..8f01a95bb3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Set; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +@FunctionalInterface +public interface PrimaryResourcesRetriever { + + Set associatedPrimaryResources(T dependentResource); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java new file mode 100644 index 0000000000..b0b9e88746 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceCache.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +@SuppressWarnings("unchecked") +public interface ResourceCache extends Cache { + + default Stream list(String namespace) { + return list(namespace, TRUE); + } + + Stream list(String namespace, Predicate predicate); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java new file mode 100644 index 0000000000..a05f6aebfe --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface ResourceEventSource

extends EventSource { + + Class getResourceClass(); + + R getAssociated(P primary); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/UpdatableCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/UpdatableCache.java new file mode 100644 index 0000000000..7e0fe56e59 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/UpdatableCache.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public interface UpdatableCache extends Cache { + T remove(ResourceID key); + + void put(ResourceID key, T resource); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java index 2397af9573..25c08d6af6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java @@ -7,9 +7,11 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; -import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.Cache; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; import static io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource.ANY_NAMESPACE_MAP_KEY; @@ -51,7 +53,8 @@ public Optional get(ResourceID resourceID) { sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); } var resource = sharedIndexInformer.getStore() - .getByKey(Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), + .getByKey(io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( + resourceID.getNamespace().orElse(null), resourceID.getName())); if (resource == null) { return Optional.empty(); @@ -60,6 +63,12 @@ public Optional get(ResourceID resourceID) { } } + @Override + public Stream keys() { + return sharedIndexInformers.values().stream() + .flatMap(i -> i.getStore().listKeys().stream().map(Mappers::fromString)); + } + private boolean isWatchingAllNamespaces() { return sharedIndexInformers.containsKey(ANY_NAMESPACE_MAP_KEY); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index a8859c8d43..f2710c3bf6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -20,13 +20,14 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; +import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; -public class ControllerResourceEventSource extends AbstractEventSource +public class ControllerResourceEventSource + extends AbstractResourceEventSource implements ResourceEventHandler { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; @@ -42,6 +43,7 @@ public class ControllerResourceEventSource extends Abstra private final ControllerResourceCache cache; public ControllerResourceEventSource(Controller controller) { + super(controller.getConfiguration().getResourceClass()); this.controller = controller; var cloner = controller.getConfiguration().getConfigurationService().getResourceCloner(); this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); @@ -90,6 +92,7 @@ public void start() { } throw e; } + super.start(); } private SharedIndexInformer createAndRunInformerFor( @@ -111,6 +114,7 @@ public void stop() { log.warn("Error stopping informer {} -> {}", controller, informer, e); } } + super.stop(); } public void eventReceived(ResourceAction action, T customResource, T oldResource) { @@ -121,7 +125,7 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource controller.getEventSourceManager().broadcastOnResourceEvent(action, customResource, oldResource); if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { - eventHandler.handleEvent( + getEventHandler().handleEvent( new ResourceEvent(action, ResourceID.fromResource(customResource))); } else { log.debug( @@ -193,4 +197,8 @@ private void handleKubernetesClientException(Exception e) { } } + @Override + public T getAssociated(T primary) { + return cache.get(ResourceID.fromResource(primary)).orElse(null); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java index 308b4a36de..1d60da2a3a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java @@ -1,9 +1,14 @@ package io.javaoperatorsdk.operator.processing.event.source.inbound; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; -public class CachingInboundEventSource extends CachingEventSource { +public class CachingInboundEventSource extends CachingEventSource { + + public CachingInboundEventSource(Class resourceClass) { + super(resourceClass); + } public void handleResourceEvent(T resource, ResourceID relatedResourceID) { super.handleEvent(resource, relatedResourceID); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java index 475cfee916..a441684f0f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java @@ -5,15 +5,15 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.LifecycleAwareEventSource; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; -public class SimpleInboundEventSource extends LifecycleAwareEventSource { +public class SimpleInboundEventSource extends AbstractEventSource { private static final Logger log = LoggerFactory.getLogger(SimpleInboundEventSource.class); public void propagateEvent(ResourceID resourceID) { if (isRunning()) { - eventHandler.handleEvent(new Event(resourceID)); + getEventHandler().handleEvent(new Event(resourceID)); } else { log.debug("Event source not started yet, not propagating event for: {}", resourceID); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 07d1e20f54..4ac4288384 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -2,8 +2,6 @@ import java.util.Objects; import java.util.Optional; -import java.util.Set; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -14,44 +12,57 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedInformer; -import io.fabric8.kubernetes.client.informers.cache.Cache; import io.fabric8.kubernetes.client.informers.cache.Store; import io.javaoperatorsdk.operator.processing.event.Event; +import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; -public class InformerEventSource extends AbstractEventSource +public class InformerEventSource + extends AbstractResourceEventSource implements ResourceCache { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); private final SharedInformer sharedInformer; - private final Function> secondaryToPrimaryResourcesIdSet; - private final Function associatedWith; + private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; + private final AssociatedSecondaryResourceIdentifier

associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToTargetResourceIDSet) { + PrimaryResourcesRetriever resourceToTargetResourceIDSet) { this(sharedInformer, resourceToTargetResourceIDSet, null, true); } public InformerEventSource(KubernetesClient client, Class type, - Function> resourceToTargetResourceIDSet) { + PrimaryResourcesRetriever resourceToTargetResourceIDSet) { this(client, type, resourceToTargetResourceIDSet, false); } + public InformerEventSource(KubernetesClient client, Class type, + PrimaryResourcesRetriever resourceToTargetResourceIDSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange) { + this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, + associatedWith, + skipUpdateEventPropagationIfNoChange); + } + InformerEventSource(KubernetesClient client, Class type, - Function> resourceToTargetResourceIDSet, + PrimaryResourcesRetriever resourceToTargetResourceIDSet, boolean skipUpdateEventPropagationIfNoChange) { this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, null, skipUpdateEventPropagationIfNoChange); } public InformerEventSource(SharedInformer sharedInformer, - Function> resourceToTargetResourceIDSet, - Function associatedWith, + PrimaryResourcesRetriever resourceToTargetResourceIDSet, + AssociatedSecondaryResourceIdentifier

associatedWith, boolean skipUpdateEventPropagationIfNoChange) { + super(sharedInformer.getApiTypeClass()); this.sharedInformer = sharedInformer; this.secondaryToPrimaryResourcesIdSet = resourceToTargetResourceIDSet; this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; @@ -61,11 +72,8 @@ public InformerEventSource(SharedInformer sharedInformer, "lead to non deterministic behavior."); } - this.associatedWith = Objects.requireNonNullElseGet(associatedWith, () -> cr -> { - final var metadata = cr.getMetadata(); - return getStore().getByKey(Cache.namespaceKeyFunc(metadata.getNamespace(), - metadata.getName())); - }); + this.associatedWith = + Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); sharedInformer.addEventHandler(new ResourceEventHandler<>() { @Override @@ -91,7 +99,7 @@ public void onDelete(T t, boolean b) { } private void propagateEvent(T object) { - var primaryResourceIdSet = secondaryToPrimaryResourcesIdSet.apply(object); + var primaryResourceIdSet = secondaryToPrimaryResourcesIdSet.associatedPrimaryResources(object); if (primaryResourceIdSet.isEmpty()) { return; } @@ -102,8 +110,9 @@ private void propagateEvent(T object) { * automatically started, what would cause a NullPointerException here, since an event might * be received between creation and registration. */ + final EventHandler eventHandler = getEventHandler(); if (eventHandler != null) { - this.eventHandler.handleEvent(event); + eventHandler.handleEvent(event); } }); } @@ -118,7 +127,7 @@ public void stop() { sharedInformer.close(); } - public Store getStore() { + private Store getStore() { return sharedInformer.getStore(); } @@ -129,8 +138,9 @@ public Store getStore() { * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ - public T getAssociated(HasMetadata resource) { - return associatedWith.apply(resource); + public T getAssociated(P resource) { + final var id = associatedWith.associatedSecondaryID(resource); + return get(id).orElse(null); } @@ -141,18 +151,24 @@ public SharedInformer getSharedInformer() { @Override public Optional get(ResourceID resourceID) { return Optional.ofNullable(sharedInformer.getStore() - .getByKey(Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), + .getByKey(io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( + resourceID.getNamespace().orElse(null), resourceID.getName()))); } @Override public Stream list(Predicate predicate) { - return sharedInformer.getStore().list().stream().filter(predicate); + return getStore().list().stream().filter(predicate); } @Override public Stream list(String namespace, Predicate predicate) { - return sharedInformer.getStore().list().stream() + return getStore().list().stream() .filter(v -> namespace.equals(v.getMetadata().getNamespace()) && predicate.test(v)); } + + @Override + public Stream keys() { + return getStore().listKeys().stream().map(Mappers::fromString); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index c578490147..1793275c42 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -2,34 +2,34 @@ import java.util.Collections; import java.util.Set; -import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; public class Mappers { - public static Function> fromAnnotation( + public static PrimaryResourcesRetriever fromAnnotation( String nameKey) { return fromMetadata(nameKey, null, false); } - public static Function> fromAnnotation( + public static PrimaryResourcesRetriever fromAnnotation( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, false); } - public static Function> fromLabel( + public static PrimaryResourcesRetriever fromLabel( String nameKey) { return fromMetadata(nameKey, null, true); } - public static Function> fromLabel( + public static PrimaryResourcesRetriever fromLabel( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, true); } - private static Function> fromMetadata( + private static PrimaryResourcesRetriever fromMetadata( String nameKey, String namespaceKey, boolean isLabel) { return resource -> { final var metadata = resource.getMetadata(); @@ -44,4 +44,20 @@ private static Function> fromMetadata } }; } + + public static ResourceID fromString(String cacheKey) { + if (cacheKey == null) { + return null; + } + + final String[] split = cacheKey.split("/"); + switch (split.length) { + case 1: + return new ResourceID(split[0]); + case 2: + return new ResourceID(split[1], split[0]); + default: + throw new IllegalArgumentException("Cannot extract a ResourceID from " + cacheKey); + } + } } 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 61b76a5863..fc9c158890 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 @@ -13,9 +13,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; /** * @@ -29,7 +29,7 @@ * @param related custom resource */ public class PerResourcePollingEventSource - extends CachingEventSource + extends CachingEventSource implements ResourceEventAware { private static final Logger log = LoggerFactory.getLogger(PerResourcePollingEventSource.class); @@ -37,18 +37,19 @@ public class PerResourcePollingEventSource private final Timer timer = new Timer(); private final Map timerTasks = new ConcurrentHashMap<>(); private final ResourceSupplier resourceSupplier; - private final ResourceCache resourceCache; + private final Cache resourceCache; private final Predicate registerPredicate; private final long period; public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - ResourceCache resourceCache, long period) { - this(resourceSupplier, resourceCache, period, null); + Cache resourceCache, long period, Class resourceClass) { + this(resourceSupplier, resourceCache, period, null, resourceClass); } public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - ResourceCache resourceCache, long period, - Predicate registerPredicate) { + Cache resourceCache, long period, + Predicate registerPredicate, Class resourceClass) { + super(resourceClass); this.resourceSupplier = resourceSupplier; this.resourceCache = resourceCache; this.period = period; 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 c979d35558..7ee7e888e2 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 @@ -6,11 +6,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; -public class PollingEventSource extends CachingEventSource { +public class PollingEventSource extends CachingEventSource { private static final Logger log = LoggerFactory.getLogger(PollingEventSource.class); @@ -19,7 +20,8 @@ public class PollingEventSource extends CachingEventSource { private final long period; public PollingEventSource(Supplier> supplier, - long period) { + long period, Class resourceClass) { + super(resourceClass); this.supplierToPoll = supplier; this.period = period; } @@ -42,9 +44,7 @@ public void run() { protected void getStateAndFillCache() { var values = supplierToPoll.get(); values.forEach((k, v) -> super.handleEvent(v, k)); - cache.keySet().stream() - .filter(e -> !values.containsKey(e)) - .forEach(super::handleDelete); + cache.keys().filter(e -> !values.containsKey(e)).forEach(super::handleDelete); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java index 2ab8b2f128..cadebc3deb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSource.java @@ -15,7 +15,8 @@ import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware; -public class TimerEventSource extends AbstractEventSource +public class TimerEventSource + extends AbstractEventSource implements ResourceEventAware { private static final Logger log = LoggerFactory.getLogger(TimerEventSource.class); @@ -73,7 +74,7 @@ public EventProducerTimeTask(ResourceID customResourceUid) { public void run() { if (running.get()) { log.debug("Producing event for custom resource id: {}", customResourceUid); - eventHandler.handleEvent(new Event(customResourceUid)); + getEventHandler().handleEvent(new Event(customResourceUid)); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java index 83fa9bb769..bed8e43c9d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java @@ -21,11 +21,12 @@ public static GenericRetry every10second10TimesRetry() { } public static Retry fromConfiguration(RetryConfiguration configuration) { - return new GenericRetry() - .setInitialInterval(configuration.getInitialInterval()) - .setMaxAttempts(configuration.getMaxAttempts()) - .setIntervalMultiplier(configuration.getIntervalMultiplier()) - .setMaxInterval(configuration.getMaxInterval()); + return configuration == null ? defaultLimitedExponentialRetry() + : new GenericRetry() + .setInitialInterval(configuration.getInitialInterval()) + .setMaxAttempts(configuration.getMaxAttempts()) + .setIntervalMultiplier(configuration.getIntervalMultiplier()) + .setMaxInterval(configuration.getMaxInterval()); } @Override diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index 79311a7057..0c848634d6 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -1,22 +1,38 @@ package io.javaoperatorsdk.operator.processing.event; import java.io.IOException; +import java.util.Iterator; +import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; +import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class EventSourceManagerTest { - private EventProcessor eventProcessorMock = mock(EventProcessor.class); - private EventSourceManager eventSourceManager = new EventSourceManager(eventProcessorMock); + private final EventProcessor eventHandler = mock(EventProcessor.class); + private final EventSourceManager eventSourceManager = new EventSourceManager(eventHandler); @Test public void registersEventSource() { @@ -27,13 +43,14 @@ public void registersEventSource() { Set registeredSources = eventSourceManager.getRegisteredEventSources(); assertThat(registeredSources).contains(eventSource); - verify(eventSource, times(1)).setEventHandler(eq(eventProcessorMock)); + verify(eventSource, times(1)).setEventHandler(eq(eventSourceManager.getEventHandler())); } @Test public void closeShouldCascadeToEventSources() throws IOException { EventSource eventSource = mock(EventSource.class); - EventSource eventSource2 = mock(EventSource.class); + EventSource eventSource2 = mock(TimerEventSource.class); + eventSourceManager.registerEventSource(eventSource); eventSourceManager.registerEventSource(eventSource2); @@ -46,7 +63,7 @@ public void closeShouldCascadeToEventSources() throws IOException { @Test public void startCascadesToEventSources() { EventSource eventSource = mock(EventSource.class); - EventSource eventSource2 = mock(EventSource.class); + EventSource eventSource2 = mock(TimerEventSource.class); eventSourceManager.registerEventSource(eventSource); eventSourceManager.registerEventSource(eventSource2); @@ -55,4 +72,109 @@ public void startCascadesToEventSources() { verify(eventSource, times(1)).start(); verify(eventSource2, times(1)).start(); } + + @Test + void retrievingEventSourceForClassShouldWork() { + assertTrue(eventSourceManager.getResourceEventSourceFor(null).isEmpty()); + assertTrue(eventSourceManager.getResourceEventSourceFor(Class.class).isEmpty()); + + // manager is initialized with a controller configured to handle HasMetadata + EventSourceManager manager = initManager(); + Optional source = manager.getResourceEventSourceFor(HasMetadata.class); + assertTrue(source.isPresent()); + assertTrue(source.get() instanceof ControllerResourceEventSource); + + CachingEventSource eventSource = mock(CachingEventSource.class); + when(eventSource.getResourceClass()).thenReturn(String.class); + manager.registerEventSource(eventSource); + + source = manager.getResourceEventSourceFor(String.class); + assertTrue(source.isPresent()); + assertEquals(eventSource, source.get()); + } + + @Test + void shouldNotBePossibleToAddEventSourcesForSameTypeAndQualifier() { + EventSourceManager manager = initManager(); + + CachingEventSource eventSource = mock(CachingEventSource.class); + when(eventSource.getResourceClass()).thenReturn(String.class); + when(eventSource.name()).thenReturn("name1"); + manager.registerEventSource(eventSource); + + eventSource = mock(CachingEventSource.class); + when(eventSource.getResourceClass()).thenReturn(String.class); + when(eventSource.name()).thenReturn("name1"); + final var source = eventSource; + + final var exception = assertThrows(OperatorException.class, + () -> manager.registerEventSource(source)); + final var cause = exception.getCause(); + assertTrue(cause instanceof IllegalArgumentException); + assertTrue(cause.getMessage().contains( + "An event source is already registered for the (java.lang.String, name1) class/name combination")); + } + + @Test + void retrievingAnEventSourceWhenMultipleAreRegisteredForATypeShouldRequireAQualifier() { + EventSourceManager manager = initManager(); + + CachingEventSource eventSource = mock(CachingEventSource.class); + when(eventSource.getResourceClass()).thenReturn(String.class); + when(eventSource.name()).thenReturn("name1"); + manager.registerEventSource(eventSource); + + CachingEventSource eventSource2 = mock(CachingEventSource.class); + when(eventSource2.getResourceClass()).thenReturn(String.class); + when(eventSource2.name()).thenReturn("name2"); + manager.registerEventSource(eventSource2); + + final var exception = assertThrows(IllegalArgumentException.class, + () -> manager.getResourceEventSourceFor(String.class)); + assertTrue(exception.getMessage().contains("name1")); + assertTrue(exception.getMessage().contains("name2")); + + assertTrue(manager.getResourceEventSourceFor(String.class, "name2").get().equals(eventSource2)); + assertTrue(manager.getResourceEventSourceFor(String.class, "name1").get().equals(eventSource)); + } + + @Test + void timerAndControllerEventSourcesShouldBeListedFirst() { + EventSourceManager manager = initManager(); + + CachingEventSource eventSource = mock(CachingEventSource.class); + when(eventSource.getResourceClass()).thenReturn(String.class); + manager.registerEventSource(eventSource); + + final Set sources = manager.getRegisteredEventSources(); + assertEquals(3, sources.size()); + final Iterator iterator = sources.iterator(); + for (int i = 0; i < sources.size(); i++) { + final EventSource source = iterator.next(); + switch (i) { + case 0: + assertTrue(source instanceof TimerEventSource); + break; + case 1: + assertTrue(source instanceof ControllerResourceEventSource); + break; + case 2: + assertTrue(source instanceof CachingEventSource); + break; + default: + fail(); + } + } + } + + private EventSourceManager initManager() { + final Controller controller = mock(Controller.class); + final ControllerConfiguration configuration = mock(ControllerConfiguration.class); + when(configuration.getResourceClass()).thenReturn(HasMetadata.class); + when(configuration.getConfigurationService()).thenReturn(mock(ConfigurationService.class)); + when(controller.getConfiguration()).thenReturn(configuration); + ExecutorServiceManager.init(configuration.getConfigurationService()); + var manager = new EventSourceManager(controller); + return manager; + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTest.java new file mode 100644 index 0000000000..68479225f7 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTest.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import org.junit.jupiter.api.AfterEach; + +import io.javaoperatorsdk.operator.processing.event.EventHandler; + +import static org.mockito.Mockito.mock; + +public class AbstractEventSourceTest { + protected T eventHandler; + protected S source; + + @AfterEach + public void tearDown() { + source.stop(); + } + + public void setUpSource(S source) { + setUpSource(source, true); + } + + public void setUpSource(S source, boolean start) { + setUpSource(source, (T) mock(EventHandler.class), start); + } + + public void setUpSource(S source, T eventHandler) { + setUpSource(source, eventHandler, true); + } + + public void setUpSource(S source, T eventHandler, boolean start) { + this.eventHandler = eventHandler; + this.source = source; + source.setEventHandler(eventHandler); + if (start) { + source.start(); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java index 7c264aa4ad..f0d81f47ba 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java @@ -1,9 +1,9 @@ package io.javaoperatorsdk.operator.processing.event.source; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; @@ -11,71 +11,65 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; -class CachingEventSourceTest { - - private CachingEventSource cachingEventSource; - private EventHandler eventHandlerMock = mock(EventHandler.class); +class CachingEventSourceTest extends + AbstractEventSourceTest, EventHandler> { @BeforeEach public void setup() { - cachingEventSource = new SimpleCachingEventSource(); - cachingEventSource.setEventHandler(eventHandlerMock); - cachingEventSource.start(); - } - - @AfterEach - public void tearDown() { - cachingEventSource.stop(); + setUpSource(new SimpleCachingEventSource()); } @Test public void putsNewResourceIntoCacheAndProducesEvent() { - cachingEventSource.handleEvent(testResource1(), testResource1ID()); + source.handleEvent(testResource1(), testResource1ID()); - verify(eventHandlerMock, times(1)).handleEvent(eq(new Event(testResource1ID()))); - assertThat(cachingEventSource.getCachedValue(testResource1ID())).isPresent(); + verify(eventHandler, times(1)).handleEvent(eq(new Event(testResource1ID()))); + assertThat(source.getCachedValue(testResource1ID())).isPresent(); } @Test public void propagatesEventIfResourceChanged() { var res2 = testResource1(); res2.setValue("changedValue"); - cachingEventSource.handleEvent(testResource1(), testResource1ID()); - cachingEventSource.handleEvent(res2, testResource1ID()); + source.handleEvent(testResource1(), testResource1ID()); + source.handleEvent(res2, testResource1ID()); - verify(eventHandlerMock, times(2)).handleEvent(eq(new Event(testResource1ID()))); - assertThat(cachingEventSource.getCachedValue(testResource1ID()).get()).isEqualTo(res2); + verify(eventHandler, times(2)).handleEvent(eq(new Event(testResource1ID()))); + assertThat(source.getCachedValue(testResource1ID()).get()).isEqualTo(res2); } @Test public void noEventPropagatedIfTheResourceIsNotChanged() { - cachingEventSource.handleEvent(testResource1(), testResource1ID()); - cachingEventSource.handleEvent(testResource1(), testResource1ID()); + source.handleEvent(testResource1(), testResource1ID()); + source.handleEvent(testResource1(), testResource1ID()); - verify(eventHandlerMock, times(1)).handleEvent(eq(new Event(testResource1ID()))); - assertThat(cachingEventSource.getCachedValue(testResource1ID())).isPresent(); + verify(eventHandler, times(1)).handleEvent(eq(new Event(testResource1ID()))); + assertThat(source.getCachedValue(testResource1ID())).isPresent(); } @Test public void propagatesEventOnDeleteIfThereIsPrevResourceInCache() { - cachingEventSource.handleEvent(testResource1(), testResource1ID()); - cachingEventSource.handleDelete(testResource1ID()); + source.handleEvent(testResource1(), testResource1ID()); + source.handleDelete(testResource1ID()); - verify(eventHandlerMock, times(2)).handleEvent(eq(new Event(testResource1ID()))); - assertThat(cachingEventSource.getCachedValue(testResource1ID())).isNotPresent(); + verify(eventHandler, times(2)).handleEvent(eq(new Event(testResource1ID()))); + assertThat(source.getCachedValue(testResource1ID())).isNotPresent(); } @Test public void noEventOnDeleteIfResourceWasNotInCacheBefore() { - cachingEventSource.handleDelete(testResource1ID()); + source.handleDelete(testResource1ID()); - verify(eventHandlerMock, times(0)).handleEvent(eq(new Event(testResource1ID()))); + verify(eventHandler, times(0)).handleEvent(eq(new Event(testResource1ID()))); } public static class SimpleCachingEventSource - extends CachingEventSource { + extends CachingEventSource { + public SimpleCachingEventSource() { + super(SampleExternalResource.class); + } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java index 3d2582fd3e..f82bea55c7 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java @@ -25,8 +25,7 @@ public void allowCustomResourceWhitelisted() { filter.whitelistNextEvent(ResourceID.fromResource(cr)); - assertThat(filter.acceptChange(null, - cr, cr)).isTrue(); + assertThat(filter.acceptChange(null, cr, cr)).isTrue(); } @Test @@ -35,10 +34,8 @@ public void allowCustomResourceWhitelistedOnlyOnce() { filter.whitelistNextEvent(ResourceID.fromResource(cr)); - assertThat(filter.acceptChange(null, - cr, cr)).isTrue(); - assertThat(filter.acceptChange(null, - cr, cr)).isFalse(); + assertThat(filter.acceptChange(null, cr, cr)).isTrue(); + assertThat(filter.acceptChange(null, cr, cr)).isFalse(); } } 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 be6d9f803b..913c93d301 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 @@ -40,6 +40,12 @@ public void before() { this.eventHandler = mock(EventHandler.class); } + private ControllerResourceEventSource init(Controller controller) { + var eventSource = new ControllerResourceEventSource<>(controller); + eventSource.setEventHandler(eventHandler); + return eventSource; + } + @Test public void eventFilteredByCustomPredicate() { var config = new TestControllerConfig( @@ -49,9 +55,7 @@ public void eventFilteredByCustomPredicate() { oldResource.getStatus().getConfigMapStatus(), newResource.getStatus().getConfigMapStatus())); - var controller = new TestController(config); - var eventSource = new ControllerResourceEventSource<>(controller); - eventSource.setEventHandler(eventHandler); + final var eventSource = init(new TestController(config)); TestCustomResource cr = TestUtils.testCustomResource(); cr.getMetadata().setFinalizers(List.of(FINALIZER)); @@ -77,9 +81,7 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { oldResource.getStatus().getConfigMapStatus(), newResource.getStatus().getConfigMapStatus())); - var controller = new TestController(config); - var eventSource = new ControllerResourceEventSource<>(controller); - eventSource.setEventHandler(eventHandler); + final var eventSource = init(new TestController(config)); TestCustomResource cr = TestUtils.testCustomResource(); cr.getMetadata().setFinalizers(List.of(FINALIZER)); @@ -107,9 +109,7 @@ public void observedGenerationFiltering() { when(config.getConfigurationService().getResourceCloner()) .thenReturn(ConfigurationService.DEFAULT_CLONER); - var controller = new ObservedGenController(config); - var eventSource = new ControllerResourceEventSource<>(controller); - eventSource.setEventHandler(eventHandler); + var eventSource = init(new ObservedGenController(config)); ObservedGenCustomResource cr = new ObservedGenCustomResource(); cr.setMetadata(new ObjectMeta()); @@ -138,9 +138,7 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { when(config.getConfigurationService().getResourceCloner()) .thenReturn(ConfigurationService.DEFAULT_CLONER); - var controller = new TestController(config); - var eventSource = new ControllerResourceEventSource<>(controller); - eventSource.setEventHandler(eventHandler); + final var eventSource = init(new TestController(config)); TestCustomResource cr = TestUtils.testCustomResource(); cr.getMetadata().setGeneration(1L); 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 5eb26db524..331daabf36 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 @@ -17,6 +17,7 @@ import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTest; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.ArgumentMatchers.eq; @@ -26,20 +27,18 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -class ControllerResourceEventSourceTest { +class ControllerResourceEventSourceTest extends + AbstractEventSourceTest, EventHandler> { public static final String FINALIZER = "finalizer"; private static final MixedOperation, Resource> client = mock(MixedOperation.class); - EventHandler eventHandler = mock(EventHandler.class); private TestController testController = new TestController(true); - private ControllerResourceEventSource controllerResourceEventSource = - new ControllerResourceEventSource<>(testController); @BeforeEach public void setup() { - controllerResourceEventSource.setEventHandler(eventHandler); + setUpSource(new ControllerResourceEventSource<>(testController), false); } @Test @@ -51,12 +50,10 @@ public void skipsEventHandlingIfGenerationNotIncreased() { TestCustomResource oldCustomResource = TestUtils.testCustomResource(); oldCustomResource.getMetadata().setFinalizers(List.of(FINALIZER)); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, - oldCustomResource); + source.eventReceived(ResourceAction.UPDATED, customResource, oldCustomResource); verify(eventHandler, times(1)).handleEvent(any()); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, - customResource); + source.eventReceived(ResourceAction.UPDATED, customResource, customResource); verify(eventHandler, times(1)).handleEvent(any()); } @@ -64,14 +61,12 @@ public void skipsEventHandlingIfGenerationNotIncreased() { public void dontSkipEventHandlingIfMarkedForDeletion() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(1)).handleEvent(any()); // mark for deletion customResource1.getMetadata().setDeletionTimestamp(LocalDateTime.now().toString()); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @@ -79,30 +74,26 @@ public void dontSkipEventHandlingIfMarkedForDeletion() { public void normalExecutionIfGenerationChanges() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(1)).handleEvent(any()); customResource1.getMetadata().setGeneration(2L); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @Test public void handlesAllEventIfNotGenerationAware() { - controllerResourceEventSource = + source = new ControllerResourceEventSource<>(new TestController(false)); setup(); TestCustomResource customResource1 = TestUtils.testCustomResource(); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(1)).handleEvent(any()); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(2)).handleEvent(any()); } @@ -110,8 +101,7 @@ public void handlesAllEventIfNotGenerationAware() { public void eventWithNoGenerationProcessedIfNoFinalizer() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(eventHandler, times(1)).handleEvent(any()); } @@ -120,10 +110,9 @@ public void eventWithNoGenerationProcessedIfNoFinalizer() { public void handlesNextEventIfWhitelisted() { TestCustomResource customResource = TestUtils.testCustomResource(); customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - controllerResourceEventSource.whitelistNextEvent(ResourceID.fromResource(customResource)); + source.whitelistNextEvent(ResourceID.fromResource(customResource)); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, - customResource); + source.eventReceived(ResourceAction.UPDATED, customResource, customResource); verify(eventHandler, times(1)).handleEvent(any()); } @@ -133,8 +122,7 @@ public void notHandlesNextEventIfNotWhitelisted() { TestCustomResource customResource = TestUtils.testCustomResource(); customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource, - customResource); + source.eventReceived(ResourceAction.UPDATED, customResource, customResource); verify(eventHandler, times(0)).handleEvent(any()); } @@ -143,8 +131,7 @@ public void notHandlesNextEventIfNotWhitelisted() { public void callsBroadcastsOnResourceEvents() { TestCustomResource customResource1 = TestUtils.testCustomResource(); - controllerResourceEventSource.eventReceived(ResourceAction.UPDATED, customResource1, - customResource1); + source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); verify(testController.getEventSourceManager(), times(1)) .broadcastOnResourceEvent(eq(ResourceAction.UPDATED), eq(customResource1), diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java index ddf1d505fd..43f0a6a5ac 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java @@ -8,23 +8,28 @@ import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTest; +import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceCache; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -class PerResourcePollingEventSourceTest { +class PerResourcePollingEventSourceTest extends + AbstractEventSourceTest, EventHandler> { public static final int PERIOD = 80; - private PerResourcePollingEventSource pollingEventSource; private PerResourcePollingEventSource.ResourceSupplier supplier = mock(PerResourcePollingEventSource.ResourceSupplier.class); - private ResourceCache resourceCache = mock(ResourceCache.class); - private EventHandler eventHandler = mock(EventHandler.class); + private Cache resourceCache = mock(Cache.class); private TestCustomResource testCustomResource = TestUtils.testCustomResource(); @BeforeEach @@ -33,15 +38,13 @@ public void setup() { when(supplier.getResources(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())); - pollingEventSource = - new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD); - pollingEventSource.setEventHandler(eventHandler); + setUpSource(new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, + SampleExternalResource.class)); } @Test public void pollsTheResourceAfterAwareOfIt() throws InterruptedException { - pollingEventSource.start(); - pollingEventSource.onResourceCreated(testCustomResource); + source.onResourceCreated(testCustomResource); Thread.sleep(3 * PERIOD); verify(supplier, atLeast(2)).getResources(eq(testCustomResource)); @@ -50,16 +53,15 @@ public void pollsTheResourceAfterAwareOfIt() throws InterruptedException { @Test public void registeringTaskOnAPredicate() throws InterruptedException { - pollingEventSource = new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, - testCustomResource -> testCustomResource.getMetadata().getGeneration() > 1); - pollingEventSource.setEventHandler(eventHandler); - pollingEventSource.start(); - pollingEventSource.onResourceCreated(testCustomResource); + setUpSource(new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, + testCustomResource -> testCustomResource.getMetadata().getGeneration() > 1, + SampleExternalResource.class)); + source.onResourceCreated(testCustomResource); Thread.sleep(2 * PERIOD); verify(supplier, times(0)).getResources(eq(testCustomResource)); testCustomResource.getMetadata().setGeneration(2L); - pollingEventSource.onResourceUpdated(testCustomResource, testCustomResource); + source.onResourceUpdated(testCustomResource, testCustomResource); Thread.sleep(2 * PERIOD); @@ -68,8 +70,7 @@ public void registeringTaskOnAPredicate() throws InterruptedException { @Test public void propagateEventOnDeletedResource() throws InterruptedException { - pollingEventSource.start(); - pollingEventSource.onResourceCreated(testCustomResource); + source.onResourceCreated(testCustomResource); when(supplier.getResources(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())) .thenReturn(Optional.empty()); @@ -81,16 +82,14 @@ public void propagateEventOnDeletedResource() throws InterruptedException { @Test public void getsValueFromCacheOrSupplier() throws InterruptedException { - pollingEventSource.start(); - pollingEventSource.onResourceCreated(testCustomResource); + source.onResourceCreated(testCustomResource); when(supplier.getResources(any())) .thenReturn(Optional.empty()) .thenReturn(Optional.of(SampleExternalResource.testResource1())); Thread.sleep(PERIOD / 2); - var value = - pollingEventSource.getValueFromCacheOrSupplier(ResourceID.fromResource(testCustomResource)); + var value = source.getValueFromCacheOrSupplier(ResourceID.fromResource(testCustomResource)); Thread.sleep(PERIOD * 2); 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 3a0c39243e..40fcba1371 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 @@ -4,38 +4,32 @@ import java.util.Map; import java.util.function.Supplier; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTest; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; import static org.mockito.Mockito.*; -class PollingEventSourceTest { +class PollingEventSourceTest + extends + AbstractEventSourceTest, EventHandler> { - private PollingEventSource pollingEventSource; private Supplier> supplier = mock(Supplier.class); - private EventHandler eventHandler = mock(EventHandler.class); @BeforeEach public void setup() { - pollingEventSource = new PollingEventSource<>(supplier, 50); - pollingEventSource.setEventHandler(eventHandler); - } - - @AfterEach - public void teardown() { - pollingEventSource.stop(); + setUpSource(new PollingEventSource<>(supplier, 50, SampleExternalResource.class)); } @Test public void pollsAndProcessesEvents() throws InterruptedException { when(supplier.get()).thenReturn(testResponseWithTwoValues()); - pollingEventSource.start(); Thread.sleep(100); @@ -46,7 +40,6 @@ public void pollsAndProcessesEvents() throws InterruptedException { public void propagatesEventForRemovedResources() throws InterruptedException { when(supplier.get()).thenReturn(testResponseWithTwoValues()) .thenReturn(testResponseWithOneValue()); - pollingEventSource.start(); Thread.sleep(150); @@ -56,7 +49,6 @@ public void propagatesEventForRemovedResources() throws InterruptedException { @Test public void doesNotPropagateEventIfResourceNotChanged() throws InterruptedException { when(supplier.get()).thenReturn(testResponseWithTwoValues()); - pollingEventSource.start(); Thread.sleep(250); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java index 0f259f77cb..3ae4f1bb92 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java @@ -15,84 +15,80 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTest; +import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSourceTest.CapturingEventHandler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -class TimerEventSourceTest { +class TimerEventSourceTest + extends AbstractEventSourceTest, CapturingEventHandler> { public static final int INITIAL_DELAY = 50; public static final int PERIOD = 50; - private TimerEventSource timerEventSource; - private CapturingEventHandler eventHandlerMock; @BeforeEach public void setup() { - eventHandlerMock = new CapturingEventHandler(); - - timerEventSource = new TimerEventSource<>(); - timerEventSource.setEventHandler(eventHandlerMock); - timerEventSource.start(); + setUpSource(new TimerEventSource<>(), new CapturingEventHandler()); } @Test public void schedulesOnce() { TestCustomResource customResource = TestUtils.testCustomResource(); - timerEventSource.scheduleOnce(customResource, PERIOD); + source.scheduleOnce(customResource, PERIOD); - untilAsserted(() -> assertThat(eventHandlerMock.events).hasSize(1)); - untilAsserted(PERIOD * 2, 0, () -> assertThat(eventHandlerMock.events).hasSize(1)); + untilAsserted(() -> assertThat(eventHandler.events).hasSize(1)); + untilAsserted(PERIOD * 2, 0, () -> assertThat(eventHandler.events).hasSize(1)); } @Test public void canCancelOnce() { TestCustomResource customResource = TestUtils.testCustomResource(); - timerEventSource.scheduleOnce(customResource, PERIOD); - timerEventSource.cancelOnceSchedule(ResourceID.fromResource(customResource)); + source.scheduleOnce(customResource, PERIOD); + source.cancelOnceSchedule(ResourceID.fromResource(customResource)); - untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); + untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); } @Test public void canRescheduleOnceEvent() { TestCustomResource customResource = TestUtils.testCustomResource(); - timerEventSource.scheduleOnce(customResource, PERIOD); - timerEventSource.scheduleOnce(customResource, 2 * PERIOD); + source.scheduleOnce(customResource, PERIOD); + source.scheduleOnce(customResource, 2 * PERIOD); - untilAsserted(PERIOD * 2, PERIOD, () -> assertThat(eventHandlerMock.events).hasSize(1)); + untilAsserted(PERIOD * 2, PERIOD, () -> assertThat(eventHandler.events).hasSize(1)); } @Test public void deRegistersOnceEventSources() { TestCustomResource customResource = TestUtils.testCustomResource(); - timerEventSource.scheduleOnce(customResource, PERIOD); - timerEventSource - .onResourceDeleted(customResource); + source.scheduleOnce(customResource, PERIOD); + source.onResourceDeleted(customResource); - untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); + untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); } @Test public void eventNotRegisteredIfStopped() throws IOException { TestCustomResource customResource = TestUtils.testCustomResource(); - timerEventSource.stop(); + source.stop(); assertThatExceptionOfType(IllegalStateException.class).isThrownBy( - () -> timerEventSource.scheduleOnce(customResource, PERIOD)); + () -> source.scheduleOnce(customResource, PERIOD)); } @Test public void eventNotFiredIfStopped() throws IOException { - timerEventSource.scheduleOnce(TestUtils.testCustomResource(), PERIOD); - timerEventSource.stop(); + source.scheduleOnce(TestUtils.testCustomResource(), PERIOD); + source.stop(); - untilAsserted(() -> assertThat(eventHandlerMock.events).isEmpty()); + untilAsserted(() -> assertThat(eventHandler.events).isEmpty()); } private void untilAsserted(ThrowingRunnable assertion) { @@ -118,7 +114,7 @@ private void untilAsserted(long initialDelay, long interval, ThrowingRunnable as cf.untilAsserted(assertion); } - private static class CapturingEventHandler implements EventHandler { + public static class CapturingEventHandler implements EventHandler { private final List events = new CopyOnWriteArrayList<>(); @Override diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index 8f20c44353..0ce67d30c3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -9,7 +9,7 @@ import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @ControllerConfiguration(finalizerName = NO_FINALIZER) public class ErrorStatusHandlerTestReconciler diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index b6807358f1..326326ddf7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.sample.informereventsource; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -7,15 +9,16 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializationContext; 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.junit.KubernetesClientAware; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; -import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; /** * Copies the config map value from spec into status. The main purpose is to test and demonstrate @@ -24,7 +27,7 @@ @ControllerConfiguration(finalizerName = NO_FINALIZER) public class InformerEventSourceTestCustomReconciler implements Reconciler, KubernetesClientAware, - EventSourceInitializer { + EventSourceInitializer { private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomReconciler.class); @@ -33,13 +36,12 @@ public class InformerEventSourceTestCustomReconciler implements public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; private KubernetesClient kubernetesClient; - private InformerEventSource eventSource; @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { - eventSource = new InformerEventSource<>(kubernetesClient, ConfigMap.class, - Mappers.fromAnnotation(RELATED_RESOURCE_NAME)); - eventSourceRegistry.registerEventSource(eventSource); + public List prepareEventSources( + EventSourceInitializationContext context) { + return List.of(new InformerEventSource<>(kubernetesClient, ConfigMap.class, + Mappers.fromAnnotation(RELATED_RESOURCE_NAME))); } @Override @@ -49,7 +51,7 @@ public UpdateControl reconcile( // Reading the config map from the informer not from the API // name of the config map same as custom resource for sake of simplicity - ConfigMap configMap = eventSource.getAssociated(resource); + ConfigMap configMap = context.getSecondaryResource(ConfigMap.class); String targetStatus = configMap.getData().get(TARGET_CONFIG_MAP_KEY); LOGGER.debug("Setting target status for CR: {}", targetStatus); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java index 01a1705fe4..65d7be4851 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java @@ -5,7 +5,7 @@ import io.javaoperatorsdk.operator.api.reconciler.*; -import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @ControllerConfiguration(finalizerName = NO_FINALIZER) public class ObservedGenerationTestReconciler diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index b8b0cacb04..0de3c8c4f2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -31,8 +31,8 @@ public RetryTestCustomReconciler(int numberOfExecutionFails) { } @Override - public UpdateControl reconcile( - RetryTestCustomResource resource, Context context) { + public UpdateControl reconcile(RetryTestCustomResource resource, + Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 7ecd883c01..ce60142dd1 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -4,6 +4,7 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.util.Base64; +import java.util.List; import java.util.Optional; import org.apache.commons.lang3.RandomStringUtils; @@ -15,8 +16,7 @@ import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; @@ -34,7 +34,6 @@ public class MySQLSchemaReconciler private final KubernetesClient kubernetesClient; private final MySQLDbConfig mysqlDbConfig; - PerResourcePollingEventSource perResourcePollingEventSource; public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig mysqlDbConfig) { this.kubernetesClient = kubernetesClient; @@ -42,22 +41,18 @@ public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig my } @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { - - perResourcePollingEventSource = - new PerResourcePollingEventSource<>(new SchemaPollingResourceSupplier(mysqlDbConfig), - eventSourceRegistry.getControllerResourceEventSource().getResourceCache(), POLL_PERIOD); - - eventSourceRegistry.registerEventSource(perResourcePollingEventSource); + public List prepareEventSources( + EventSourceInitializationContext context) { + return List.of(new PerResourcePollingEventSource<>( + new SchemaPollingResourceSupplier(mysqlDbConfig), context.getPrimaryCache(), POLL_PERIOD, + Schema.class)); } @Override - public UpdateControl reconcile(MySQLSchema schema, - Context context) { - var dbSchema = perResourcePollingEventSource - .getValueFromCacheOrSupplier(ResourceID.fromResource(schema)); + public UpdateControl reconcile(MySQLSchema schema, Context context) { + var dbSchema = context.getSecondaryResource(Schema.class); try (Connection connection = getConnection()) { - if (!dbSchema.isPresent()) { + if (dbSchema != null) { var schemaName = schema.getMetadata().getName(); String password = RandomStringUtils.randomAlphanumeric(16); String secretName = String.format(SECRET_FORMAT, schemaName); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 1d2c8d187b..a9802a6d21 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -17,10 +18,10 @@ import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; import static java.util.Collections.EMPTY_SET; /** @@ -34,29 +35,27 @@ public class TomcatReconciler implements Reconciler, EventSourceInitiali private final KubernetesClient kubernetesClient; - private volatile InformerEventSource informerEventSource; - public TomcatReconciler(KubernetesClient client) { this.kubernetesClient = client; } @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { + public List prepareEventSources(EventSourceInitializationContext context) { SharedIndexInformer deploymentInformer = kubernetesClient.apps().deployments().inAnyNamespace() .withLabel("app.kubernetes.io/managed-by", "tomcat-operator") .runnableInformer(0); - this.informerEventSource = new InformerEventSource<>(deploymentInformer, d -> { - var ownerReferences = d.getMetadata().getOwnerReferences(); - if (!ownerReferences.isEmpty()) { - return Set.of(new ResourceID(ownerReferences.get(0).getName(), - d.getMetadata().getNamespace())); - } else { - return EMPTY_SET; - } - }); - eventSourceRegistry.registerEventSource(this.informerEventSource); + return List.of(new InformerEventSource<>( + deploymentInformer, d -> { + var ownerReferences = d.getMetadata().getOwnerReferences(); + if (!ownerReferences.isEmpty()) { + return Set.of(new ResourceID(ownerReferences.get(0).getName(), + d.getMetadata().getNamespace())); + } else { + return EMPTY_SET; + } + })); } @Override @@ -64,7 +63,7 @@ public UpdateControl reconcile(Tomcat tomcat, Context context) { createOrUpdateDeployment(tomcat); createOrUpdateService(tomcat); - Deployment deployment = informerEventSource.getAssociated(tomcat); + Deployment deployment = context.getSecondaryResource(Deployment.class); if (deployment != null) { Tomcat updatedTomcat = diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index d06740c4b4..70297573dc 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -17,15 +17,15 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; -import io.fabric8.kubernetes.client.informers.cache.Cache; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializationContext; 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.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceRegistry; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import okhttp3.Response; @@ -41,22 +41,22 @@ public WebappReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } - private InformerEventSource tomcatEventSource; - @Override - public void prepareEventSources(EventSourceRegistry eventSourceRegistry) { - tomcatEventSource = - new InformerEventSource<>(kubernetesClient, Tomcat.class, t -> { + public List prepareEventSources(EventSourceInitializationContext context) { + return List.of(new InformerEventSource<>( + kubernetesClient, Tomcat.class, t -> { // To create an event to a related WebApp resource and trigger the reconciliation // we need to find which WebApp this Tomcat custom resource is related to. // To find the related customResourceId of the WebApp resource we traverse the cache to // and identify it based on naming convention. - return eventSourceRegistry.getControllerResourceEventSource().getResourceCache() + return context.getPrimaryCache() .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) .map(ResourceID::fromResource) .collect(Collectors.toSet()); - }); - eventSourceRegistry.registerEventSource(tomcatEventSource); + }, + (Webapp webapp) -> new ResourceID(webapp.getSpec().getTomcat(), + webapp.getMetadata().getNamespace()), + true)); } /** @@ -70,9 +70,7 @@ public UpdateControl reconcile(Webapp webapp, Context context) { return UpdateControl.noUpdate(); } - Tomcat tomcat = tomcatEventSource.getStore() - .getByKey(Cache.namespaceKeyFunc(webapp.getMetadata().getNamespace(), - webapp.getSpec().getTomcat())); + Tomcat tomcat = context.getSecondaryResource(Tomcat.class); if (tomcat == null) { throw new IllegalStateException("Cannot find Tomcat " + webapp.getSpec().getTomcat() + " for Webapp " + webapp.getMetadata().getName() + " in namespace " From fd525907912e5a31a742928f7a9e8f6cadba79e1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 22 Dec 2021 17:46:59 +0100 Subject: [PATCH 0193/1608] chore(docs): fix javadoc and improve it (#774) --- .../EventSourceInitializationContext.java | 11 ++++++++++ .../reconciler/EventSourceInitializer.java | 4 ++-- .../processing/event/source/EventSource.java | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java index f2a053552c..4252689185 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java @@ -1,8 +1,14 @@ package io.javaoperatorsdk.operator.api.reconciler; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +/** + * Contextual information made available to prepare event sources. + * + * @param

the type associated with the primary resource that is handled by your reconciler + */ public class EventSourceInitializationContext

{ private final ResourceCache

primaryCache; @@ -11,6 +17,11 @@ public EventSourceInitializationContext(ResourceCache

primaryCache) { this.primaryCache = primaryCache; } + /** + * Retrieves the cache that an {@link EventSource} can query to retrieve primary resources + * + * @return the primary resource cache + */ public ResourceCache

getPrimaryCache() { return primaryCache; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 7c8d8e9f7c..3a85326041 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -16,8 +16,8 @@ public interface EventSourceInitializer

{ /** * Prepares a list of {@link EventSource} implementations to be registered by the SDK. * - * @param primaryCache a cache providing direct access to primary resources so that event sources - * can extract relevant information from primary resources as needed + * @param context a {@link EventSourceInitializationContext} providing access to information + * useful to event sources */ List prepareEventSources(EventSourceInitializationContext

context); 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 8d606fbd21..04e19f7ceb 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 @@ -3,10 +3,30 @@ import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.EventHandler; +/** + * Creates an event source to trigger your reconciler whenever something happens to a secondary or + * external resource that would not normally trigger your reconciler (as the primary resources are + * not changed). To register EventSources with so that your reconciler is triggered, please make + * your reconciler implement + * {@link io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer}. + */ public interface EventSource extends LifecycleAware { + + /** + * An optional name for your EventSource. This is only required if you need to register multiple + * EventSources for the same resource type (e.g. {@code Deployment}). + * + * @return the name associated with this EventSource + */ default String name() { return getClass().getCanonicalName(); } + /** + * Sets the {@link EventHandler} that is linked to your reconciler when this EventSource is + * registered. + * + * @param handler the {@link EventHandler} associated with your reconciler + */ void setEventHandler(EventHandler handler); } From 962086b91655077d48b3d026a2518cc27b93c15b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Sat, 25 Dec 2021 17:40:39 +0100 Subject: [PATCH 0194/1608] fix: make org & io import order stable (#777) --- contributing/eclipse.importorder | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributing/eclipse.importorder b/contributing/eclipse.importorder index 8f15ca7dd3..8a156041e9 100644 --- a/contributing/eclipse.importorder +++ b/contributing/eclipse.importorder @@ -1,7 +1,7 @@ 0=java 1=javax 2=org -2=io +3=io 4=com 5= 6=\# From 6c4b9e5e91c85bb2245c30b6a7b6d0386902bca5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Dec 2021 19:49:05 +0100 Subject: [PATCH 0195/1608] chore(deps): bump kubernetes-client-bom from 5.11.0 to 5.11.1 (#778) Bumps [kubernetes-client-bom](https://github.com/fabric8io/kubernetes-client) from 5.11.0 to 5.11.1. - [Release notes](https://github.com/fabric8io/kubernetes-client/releases) - [Changelog](https://github.com/fabric8io/kubernetes-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/fabric8io/kubernetes-client/compare/v5.11.0...v5.11.1) --- updated-dependencies: - dependency-name: io.fabric8:kubernetes-client-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7e4dad678a..93f5a14fc4 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ ${java.version} 5.8.2 - 5.11.0 + 5.11.1 1.7.32 2.17.0 4.2.0 From 91fdfaf487ad2aee0555af3cbfa2187a73794f2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Dec 2021 10:07:08 +0100 Subject: [PATCH 0196/1608] chore(deps): bump log4j.version from 2.17.0 to 2.17.1 (#779) Bumps `log4j.version` from 2.17.0 to 2.17.1. Updates `log4j-slf4j-impl` from 2.17.0 to 2.17.1 Updates `log4j-core` from 2.17.0 to 2.17.1 Updates `log4j-api` from 2.17.0 to 2.17.1 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-slf4j-impl dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-api dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 93f5a14fc4..e21270a6de 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 5.8.2 5.11.1 1.7.32 - 2.17.0 + 2.17.1 4.2.0 3.12.0 1.0.1 From 4bfc346c7d907f89d4853fecd029ce40301c6523 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 09:26:26 +0100 Subject: [PATCH 0197/1608] chore(deps): bump assertj-core from 3.21.0 to 3.22.0 (#780) Bumps [assertj-core](https://github.com/assertj/assertj-core) from 3.21.0 to 3.22.0. - [Release notes](https://github.com/assertj/assertj-core/releases) - [Commits](https://github.com/assertj/assertj-core/compare/assertj-core-3.21.0...assertj-core-3.22.0) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e21270a6de..9c585c837b 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ 1.0.1 0.19 1.13.0 - 3.21.0 + 3.22.0 4.1.1 2.6.2 1.8.1 From f40023cada81ea3cdda7fe21e88899575ee61226 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 3 Jan 2022 10:16:22 +0100 Subject: [PATCH 0198/1608] feat: initialization context provides access to ConfigurationService (#776) --- .../EventSourceInitializationContext.java | 19 ++++++++++++++++++- .../operator/processing/Controller.java | 3 ++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java index 4252689185..a9b409bff1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.reconciler; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; @@ -12,9 +13,12 @@ public class EventSourceInitializationContext

{ private final ResourceCache

primaryCache; + private final ConfigurationService configurationService; - public EventSourceInitializationContext(ResourceCache

primaryCache) { + public EventSourceInitializationContext(ResourceCache

primaryCache, + ConfigurationService configurationService) { this.primaryCache = primaryCache; + this.configurationService = configurationService; } /** @@ -25,4 +29,17 @@ public EventSourceInitializationContext(ResourceCache

primaryCache) { public ResourceCache

getPrimaryCache() { return primaryCache; } + + /** + * Retrieves the {@link ConfigurationService} associated with the operator. This allows, in + * particular, to lookup global configuration information such as the configured + * {@link io.javaoperatorsdk.operator.api.monitoring.Metrics} or + * {@link io.javaoperatorsdk.operator.api.config.Cloner} implementations, which could be useful to + * event sources. + * + * @return the {@link ConfigurationService} associated with the operator + */ + public ConfigurationService getConfigurationService() { + return configurationService; + } } 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 ba5035c712..92546b5d1a 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 @@ -173,7 +173,8 @@ public void start() throws OperatorException { if (reconciler instanceof EventSourceInitializer) { ((EventSourceInitializer) reconciler) .prepareEventSources(new EventSourceInitializationContext<>( - eventSourceManager.getControllerResourceEventSource().getResourceCache())) + eventSourceManager.getControllerResourceEventSource().getResourceCache(), + configuration.getConfigurationService())) .forEach(eventSourceManager::registerEventSource); } if (failOnMissingCurrentNS()) { From 2bbace6d724097dd79205451b38d417b3931e81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 3 Jan 2022 16:38:43 +0100 Subject: [PATCH 0199/1608] fix: API review, minor changes (#775) --- .../operator/api/reconciler/Context.java | 6 ++- .../api/reconciler/DefaultContext.java | 4 +- .../processing/event/EventSourceManager.java | 39 +++++++++++-------- .../operator/ControllerManagerTest.java | 3 +- .../event/EventSourceManagerTest.java | 23 ++++++----- .../sample/simple/TestCustomResource.java | 2 + .../sample/simple/TestCustomResourceV2.java | 2 + .../sample/MySQLSchemaReconciler.java | 2 +- 8 files changed, 48 insertions(+), 33 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 4c716c2842..367de503d1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -6,5 +6,9 @@ public interface Context { Optional getRetryInfo(); - T getSecondaryResource(Class expectedType, String... qualifier); + default T getSecondaryResource(Class expectedType) { + return getSecondaryResource(expectedType, null); + } + + T getSecondaryResource(Class expectedType, String eventSourceName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 7aa856097e..8ec9f2c39b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -23,9 +23,9 @@ public Optional getRetryInfo() { } @Override - public T getSecondaryResource(Class expectedType, String... qualifier) { + public T getSecondaryResource(Class expectedType, String eventSourceName) { final var eventSource = - controller.getEventSourceManager().getResourceEventSourceFor(expectedType, qualifier); + controller.getEventSourceManager().getResourceEventSourceFor(expectedType, eventSourceName); return eventSource.map(es -> es.getAssociated(primaryResource)).orElse(null); } } 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 fb62b8b22c..bdd7504d35 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 @@ -140,11 +140,16 @@ public ControllerResourceEventSource getControllerResourceEventSource() { } public Optional> getResourceEventSourceFor( - Class dependentType, String... qualifier) { + Class dependentType) { + return getResourceEventSourceFor(dependentType, null); + } + + public Optional> getResourceEventSourceFor( + Class dependentType, String qualifier) { if (dependentType == null) { return Optional.empty(); } - String name = (qualifier != null && qualifier.length >= 1) ? qualifier[0] : ""; + String name = qualifier == null ? "" : qualifier; final var eventSource = eventSources.get(dependentType, name); return Optional.ofNullable(eventSource); } @@ -192,13 +197,13 @@ public boolean contains(EventSource source) { if (eventSources == null || eventSources.isEmpty()) { return false; } - return findMatchingSource(qualifier(source), eventSources).isPresent(); + return findMatchingSource(name(source), eventSources).isPresent(); } public void add(EventSource eventSource) { if (contains(eventSource)) { throw new IllegalArgumentException("An event source is already registered for the " - + keyAsString(getDependentType(eventSource), qualifier(eventSource)) + + keyAsString(getDependentType(eventSource), name(eventSource)) + " class/name combination"); } sources.computeIfAbsent(keyFor(eventSource), k -> new ArrayList<>()).add(eventSource); @@ -210,7 +215,7 @@ private Class getDependentType(EventSource source) { : source.getClass(); } - private String qualifier(EventSource source) { + private String name(EventSource source) { return source.name(); } @@ -233,7 +238,7 @@ private String keyFor(Class dependentType) { return key; } - public ResourceEventSource get(Class dependentType, String qualifier) { + public ResourceEventSource get(Class dependentType, String name) { final var sourcesForType = sources.get(keyFor(dependentType)); if (sourcesForType == null || sourcesForType.isEmpty()) { return null; @@ -244,13 +249,13 @@ public ResourceEventSource get(Class dependentType, String qualifie if (size == 1) { source = sourcesForType.get(0); } else { - if (qualifier == null || qualifier.isBlank()) { + if (name == null || name.isBlank()) { throw new IllegalArgumentException("There are multiple EventSources registered for type " + dependentType.getCanonicalName() - + ", you need to provide a qualifier to specify which EventSource you want to query. Known qualifiers: " - + sourcesForType.stream().map(this::qualifier).collect(Collectors.joining(","))); + + ", you need to provide a name to specify which EventSource you want to query. Known names: " + + sourcesForType.stream().map(this::name).collect(Collectors.joining(","))); } - source = findMatchingSource(qualifier, sourcesForType).orElse(null); + source = findMatchingSource(name, sourcesForType).orElse(null); if (source == null) { return null; @@ -259,29 +264,29 @@ public ResourceEventSource get(Class dependentType, String qualifie if (!(source instanceof ResourceEventSource)) { throw new IllegalArgumentException(source + " associated with " - + keyAsString(dependentType, qualifier) + " is not a " + + keyAsString(dependentType, name) + " is not a " + ResourceEventSource.class.getSimpleName()); } final var res = (ResourceEventSource) source; final var resourceClass = res.getResourceClass(); if (!resourceClass.isAssignableFrom(dependentType)) { throw new IllegalArgumentException(source + " associated with " - + keyAsString(dependentType, qualifier) + + keyAsString(dependentType, name) + " is handling " + resourceClass.getName() + " resources but asked for " + dependentType.getName()); } return res; } - private Optional findMatchingSource(String qualifier, + private Optional findMatchingSource(String name, List sourcesForType) { - return sourcesForType.stream().filter(es -> qualifier(es).equals(qualifier)).findAny(); + return sourcesForType.stream().filter(es -> name(es).equals(name)).findAny(); } @SuppressWarnings("rawtypes") - private String keyAsString(Class dependentType, String qualifier) { - return qualifier != null && qualifier.length() > 0 - ? "(" + dependentType.getName() + ", " + qualifier + ")" + private String keyAsString(Class dependentType, String name) { + return name != null && name.length() > 0 + ? "(" + dependentType.getName() + ", " + name + ")" : dependentType.getName(); } } 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 c24a6dbe3e..a1cfa3e82d 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 @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator; -import org.junit.Test; +import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.CustomResource; @@ -37,7 +37,6 @@ public void addingMultipleControllersForCustomResourcesWithDifferentVersionsShou TestCustomResourceV2.class); checkException(registered, duplicated); - } private void checkException( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index 0c848634d6..b3bf230640 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -17,6 +17,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -94,16 +95,16 @@ void retrievingEventSourceForClassShouldWork() { } @Test - void shouldNotBePossibleToAddEventSourcesForSameTypeAndQualifier() { + void shouldNotBePossibleToAddEventSourcesForSameTypeAndName() { EventSourceManager manager = initManager(); CachingEventSource eventSource = mock(CachingEventSource.class); - when(eventSource.getResourceClass()).thenReturn(String.class); + when(eventSource.getResourceClass()).thenReturn(TestCustomResource.class); when(eventSource.name()).thenReturn("name1"); manager.registerEventSource(eventSource); eventSource = mock(CachingEventSource.class); - when(eventSource.getResourceClass()).thenReturn(String.class); + when(eventSource.getResourceClass()).thenReturn(TestCustomResource.class); when(eventSource.name()).thenReturn("name1"); final var source = eventSource; @@ -111,8 +112,8 @@ void shouldNotBePossibleToAddEventSourcesForSameTypeAndQualifier() { () -> manager.registerEventSource(source)); final var cause = exception.getCause(); assertTrue(cause instanceof IllegalArgumentException); - assertTrue(cause.getMessage().contains( - "An event source is already registered for the (java.lang.String, name1) class/name combination")); + assertThat(cause.getMessage()).contains( + "An event source is already registered for the (io.javaoperatorsdk.operator.sample.simple.TestCustomResource, name1) class/name combination"); } @Test @@ -120,22 +121,24 @@ void retrievingAnEventSourceWhenMultipleAreRegisteredForATypeShouldRequireAQuali EventSourceManager manager = initManager(); CachingEventSource eventSource = mock(CachingEventSource.class); - when(eventSource.getResourceClass()).thenReturn(String.class); + when(eventSource.getResourceClass()).thenReturn(TestCustomResource.class); when(eventSource.name()).thenReturn("name1"); manager.registerEventSource(eventSource); CachingEventSource eventSource2 = mock(CachingEventSource.class); - when(eventSource2.getResourceClass()).thenReturn(String.class); + when(eventSource2.getResourceClass()).thenReturn(TestCustomResource.class); when(eventSource2.name()).thenReturn("name2"); manager.registerEventSource(eventSource2); final var exception = assertThrows(IllegalArgumentException.class, - () -> manager.getResourceEventSourceFor(String.class)); + () -> manager.getResourceEventSourceFor(TestCustomResource.class)); assertTrue(exception.getMessage().contains("name1")); assertTrue(exception.getMessage().contains("name2")); - assertTrue(manager.getResourceEventSourceFor(String.class, "name2").get().equals(eventSource2)); - assertTrue(manager.getResourceEventSourceFor(String.class, "name1").get().equals(eventSource)); + assertTrue(manager.getResourceEventSourceFor(TestCustomResource.class, "name2").get() + .equals(eventSource2)); + assertTrue(manager.getResourceEventSourceFor(TestCustomResource.class, "name1").get() + .equals(eventSource)); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java index d01bd3c747..65649bff57 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java @@ -2,10 +2,12 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; import io.fabric8.kubernetes.model.annotation.Version; @Group("sample.javaoperatorsdk.io") @Version("v1") +@Kind("TestCustomResource") public class TestCustomResource extends CustomResource { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java index e02e359bcc..6a5385c0f9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java @@ -2,10 +2,12 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; import io.fabric8.kubernetes.model.annotation.Version; @Group("sample.javaoperatorsdk.io") @Version("v2") +@Kind("TestCustomResource") public class TestCustomResourceV2 extends CustomResource { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index ce60142dd1..ce9d062927 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -52,7 +52,7 @@ public List prepareEventSources( public UpdateControl reconcile(MySQLSchema schema, Context context) { var dbSchema = context.getSecondaryResource(Schema.class); try (Connection connection = getConnection()) { - if (dbSchema != null) { + if (dbSchema == null) { var schemaName = schema.getMetadata().getName(); String password = RandomStringUtils.randomAlphanumeric(16); String secretName = String.format(SECRET_FORMAT, schemaName); From db359ad4b2f7dd6f81029e52a7c63d15460c3c6e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 3 Jan 2022 16:59:10 +0100 Subject: [PATCH 0200/1608] Minor fixes (#783) * fix: remove unneeded Kind annotation and add comment where it's needed * fix: toString methods were incorrect --- .../io/javaoperatorsdk/operator/processing/event/Event.java | 2 +- .../processing/event/source/controller/ResourceEvent.java | 3 ++- .../operator/sample/simple/TestCustomResource.java | 2 -- .../operator/sample/simple/TestCustomResourceV2.java | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java index 0f13b71e11..227a297795 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java @@ -16,7 +16,7 @@ public ResourceID getRelatedCustomResourceID() { @Override public String toString() { - return "DefaultEvent{" + + return "Event{" + "relatedCustomResource=" + relatedCustomResource + '}'; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java index ad1d85330c..15424fadb4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEvent.java @@ -15,8 +15,9 @@ public ResourceEvent(ResourceAction action, @Override public String toString() { - return "CustomResourceEvent{" + + return "ResourceEvent{" + "action=" + action + + ", associated resource id=" + getRelatedCustomResourceID() + '}'; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java index 65649bff57..d01bd3c747 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java @@ -2,12 +2,10 @@ import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; -import io.fabric8.kubernetes.model.annotation.Kind; import io.fabric8.kubernetes.model.annotation.Version; @Group("sample.javaoperatorsdk.io") @Version("v1") -@Kind("TestCustomResource") public class TestCustomResource extends CustomResource { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java index 6a5385c0f9..d4c71e0662 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java @@ -7,7 +7,7 @@ @Group("sample.javaoperatorsdk.io") @Version("v2") -@Kind("TestCustomResource") +@Kind("TestCustomResource") // this is needed to override the automatically generated kind public class TestCustomResourceV2 extends CustomResource { From a499e5c6f5e8470edc3f44a9bc2e62c9527b8052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 4 Jan 2022 08:56:18 +0100 Subject: [PATCH 0201/1608] Secondary resource access with Optional (#782) --- .../operator/api/reconciler/Context.java | 4 +-- .../api/reconciler/DefaultContext.java | 4 +-- ...formerEventSourceTestCustomReconciler.java | 3 +- .../operator/sample/TomcatReconciler.java | 32 +++++++++++-------- .../operator/sample/WebappReconciler.java | 11 +++---- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 367de503d1..d697325219 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -6,9 +6,9 @@ public interface Context { Optional getRetryInfo(); - default T getSecondaryResource(Class expectedType) { + default Optional getSecondaryResource(Class expectedType) { return getSecondaryResource(expectedType, null); } - T getSecondaryResource(Class expectedType, String eventSourceName); + Optional getSecondaryResource(Class expectedType, String eventSourceName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 8ec9f2c39b..05e185940c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -23,9 +23,9 @@ public Optional getRetryInfo() { } @Override - public T getSecondaryResource(Class expectedType, String eventSourceName) { + public Optional getSecondaryResource(Class expectedType, String eventSourceName) { final var eventSource = controller.getEventSourceManager().getResourceEventSourceFor(expectedType, eventSourceName); - return eventSource.map(es -> es.getAssociated(primaryResource)).orElse(null); + return eventSource.map(es -> es.getAssociated(primaryResource)); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 326326ddf7..647903abc4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -51,7 +51,8 @@ public UpdateControl reconcile( // Reading the config map from the informer not from the API // name of the config map same as custom resource for sake of simplicity - ConfigMap configMap = context.getSecondaryResource(ConfigMap.class); + ConfigMap configMap = context.getSecondaryResource(ConfigMap.class) + .orElseThrow(() -> new IllegalStateException("Config map should be present.")); String targetStatus = configMap.getData().get(TARGET_CONFIG_MAP_KEY); LOGGER.debug("Setting target status for CR: {}", targetStatus); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index a9802a6d21..a4c4272a17 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -16,7 +16,12 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; -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.EventSourceInitializationContext; +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.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; @@ -63,19 +68,18 @@ public UpdateControl reconcile(Tomcat tomcat, Context context) { createOrUpdateDeployment(tomcat); createOrUpdateService(tomcat); - Deployment deployment = context.getSecondaryResource(Deployment.class); - - if (deployment != null) { - Tomcat updatedTomcat = - updateTomcatStatus(tomcat, deployment); - log.info( - "Updating status of Tomcat {} in namespace {} to {} ready replicas", - tomcat.getMetadata().getName(), - tomcat.getMetadata().getNamespace(), - tomcat.getStatus().getReadyReplicas()); - return UpdateControl.updateStatus(updatedTomcat); - } - return UpdateControl.noUpdate(); + return context.getSecondaryResource(Deployment.class) + .map(deployment -> { + Tomcat updatedTomcat = + updateTomcatStatus(tomcat, deployment); + log.info( + "Updating status of Tomcat {} in namespace {} to {} ready replicas", + tomcat.getMetadata().getName(), + tomcat.getMetadata().getNamespace(), + tomcat.getStatus().getReadyReplicas()); + return UpdateControl.updateStatus(updatedTomcat); + }) + .orElse(UpdateControl.noUpdate()); } private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 70297573dc..730ef1ca89 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -70,12 +70,11 @@ public UpdateControl reconcile(Webapp webapp, Context context) { return UpdateControl.noUpdate(); } - Tomcat tomcat = context.getSecondaryResource(Tomcat.class); - if (tomcat == null) { - throw new IllegalStateException("Cannot find Tomcat " + webapp.getSpec().getTomcat() - + " for Webapp " + webapp.getMetadata().getName() + " in namespace " - + webapp.getMetadata().getNamespace()); - } + Tomcat tomcat = context.getSecondaryResource(Tomcat.class) + .orElseThrow( + () -> new IllegalStateException("Cannot find Tomcat " + webapp.getSpec().getTomcat() + + " for Webapp " + webapp.getMetadata().getName() + " in namespace " + + webapp.getMetadata().getNamespace())); if (tomcat.getStatus() != null && Objects.equals(tomcat.getSpec().getReplicas(), tomcat.getStatus().getReadyReplicas())) { From 12f6125024d15c9a3963ba4fab2b75687d30d992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 4 Jan 2022 09:49:49 +0100 Subject: [PATCH 0202/1608] docs: samples facelift (#784) --- docs/documentation/getting-started.md | 55 ------------------- docs/documentation/patterns-best-practices.md | 2 +- docs/documentation/use-samples.md | 36 ++++++++---- 3 files changed, 26 insertions(+), 67 deletions(-) diff --git a/docs/documentation/getting-started.md b/docs/documentation/getting-started.md index 04f9b68b7d..649d454a45 100644 --- a/docs/documentation/getting-started.md +++ b/docs/documentation/getting-started.md @@ -38,60 +38,5 @@ to the cluster. This is why it was important to set up kubectl up front. 1. Verify if the operator is up and running. Don't run it locally anymore to avoid conflicts in processing events from the cluster's API server. -## Controllers -Controllers are where you implement the business logic of the Operator. An Operator can host multiple Controllers, -each handling a different type of Custom Resource. In our samples each Operator has a single Controller. -[comment]: <> (## Automatic Retries) -[comment]: <> (## Running The Operator) - -[comment]: <> (## Development Tips & Tricks) - -[comment]: <> (TODO: explain running operator locally against a cluster) - -[comment]: <> (## Event Processing Details) - -[comment]: <> (### Handling Finalizers) - -[comment]: <> (### Managing Consistency) - -[comment]: <> (### Event Scheduling) - -[comment]: <> (### Event Dispatching ) - -[comment]: <> (### Generation Awareness) - -## Dealing with Consistency - -### Run Single Instance - -There should be always just one instance of an operator running at a time (think process). If there would be -two ore more, in general it could lead to concurrency issues. Note that we are also doing optimistic locking when we update a resource. -In this way the operator is not highly available. However for operators this is not necessarily an issue, -if the operator just gets restarted after it went down. - -### At Least Once - -To implement controller logic, we have to override two methods: `createOrUpdateResource` and `deleteResource`. -These methods are called if a resource is created/changed or marked for deletion. In most cases these methods will be -called just once, but in some rare cases, it can happen that they are called more then once. In practice this means that the -implementation needs to be **idempotent**. - -### Smart Scheduling - -In our scheduling algorithm we make sure, that no events are processed concurrently for a resource. In addition we provide -a customizable retry mechanism to deal with temporal errors. - -### Operator Restarts - -When an operator is started we got events for every resource (of a type we listen to) already on the cluster. Even if the resource is not changed -(We use `kubectl get ... --watch` in the background). This can be a huge amount of resources depending on your use case. -So it could be a good case to just have a status field on the resource which is checked, if there is anything needed to be done. - -### Deleting a Resource - -During deletion process we use [Kubernetes finalizers](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#finalizers -"Kubernetes docs"). This is required, since it can happen that the operator is not running while the delete -of resource is executed (think `oc delete`). In this case we would not catch the delete event. So we automatically add a -finalizer first time we update the resource if it's not there. diff --git a/docs/documentation/patterns-best-practices.md b/docs/documentation/patterns-best-practices.md index 13de8ae910..519045f5d3 100644 --- a/docs/documentation/patterns-best-practices.md +++ b/docs/documentation/patterns-best-practices.md @@ -10,7 +10,7 @@ permalink: /docs/patterns-best-practices This document describes patters and best practices, to build and run operators, and how to implement them in terms of Java Operator SDK. -## Implementing a Controller +## Implementing a Reconciler ### Idempotency diff --git a/docs/documentation/use-samples.md b/docs/documentation/use-samples.md index 04988c076e..616a782181 100644 --- a/docs/documentation/use-samples.md +++ b/docs/documentation/use-samples.md @@ -5,20 +5,26 @@ layout: docs permalink: /docs/using-samples --- -# How to use sample Operators +# Sample Operators we Provide -We have several sample Operators under -the [samples](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/smoke-test-samples) directory: +We have few simple Operators under +the [smoke-test-samples](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/smoke-test-samples) directory. +These are used mainly to showcase some minimal operators, but also to do some sanity checks during development: * *pure-java*: Minimal Operator implementation which only parses the Custom Resource and prints to stdout. Implemented with and without Spring Boot support. The two samples share the common module. * *spring-boot-plain*: Sample showing integration with Spring Boot. -There are also more samples in the standalone [samples repo](https://github.com/java-operator-sdk/samples): +In addition to that, there are examples under [sample-operators](https://github.com/java-operator-sdk/java-operator-sdk/tree/master/sample-operators) +directory which are intended to show usage of different components in different scenarios, but mainly are more real world +examples: -* *webserver*: Simple example creating an NGINX webserver from a Custom Resource containing HTML code. -* *mysql-schema*: Operator managing schemas in a MySQL database. -* *tomcat*: Operator with two controllers, managing Tomcat instances and Webapps for these. +* *webpage*: Simple example creating an NGINX webserver from a Custom Resource containing HTML code. +* *mysql-schema*: Operator managing schemas in a MySQL database. Shows how to manage non Kubernetes resources. +* *tomcat*: Operator with two controllers, managing Tomcat instances and Webapps running in Tomcat. The intention + with this example to show how to manage multiple related custom resources and/or more controllers. + +# Implementing a Sample Operator Add [dependency](https://search.maven.org/search?q=a:operator-framework) to your project with Maven: @@ -59,7 +65,7 @@ The Controller implements the business logic and describes all the classes neede ```java @ControllerConfiguration -public class WebServerController implements Reconciler { +public class WebPageReconciler implements Reconciler { // Return the changed resource, so it gets updated. See javadoc for details. @Override @@ -77,9 +83,8 @@ A sample custom resource POJO representation @Group("sample.javaoperatorsdk") @Version("v1") -public class WebServer extends CustomResource implements +public class WebPage extends CustomResource implements Namespaced { - } public class WebServerSpec { @@ -107,7 +112,7 @@ might want to skip this step. This is done by setting the `CHECK_CRD_ENV_KEY` en ### Automatic generation of CRDs To automatically generate CRD manifests from your annotated Custom Resource classes, you only need to add the following -dependencies to your project: +dependencies to your project (in the background an annotation processor is used), with Maven: ```xml @@ -118,6 +123,15 @@ dependencies to your project: ``` +or with Gradle: + +```groovy +dependencies { + annotationProcessor 'io.fabric8:crd-generator-apt:' + ... +} +``` + The CRD will be generated in `target/classes/META-INF/fabric8` (or in `target/test-classes/META-INF/fabric8`, if you use the `test` scope) with the CRD name suffixed by the generated spec version. For example, a CR using the `java-operator-sdk.io` group with a `mycrs` plural form will result in 2 files: From 781bd35c68d99c879a7b1045f7e0b0291a305c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 4 Jan 2022 16:49:54 +0100 Subject: [PATCH 0203/1608] mysql e2e test (#786) --- .github/workflows/e2e-test-mysql.yml | 80 +++++++++++++++++++ ...d-to-end-tests.yml => e2e-test-tomcat.yml} | 7 +- .../PerResourcePollingEventSource.java | 7 +- .../mysql-schema/k8s/mysql-deployment.yaml | 1 + .../mysql-schema/k8s/mysql-service.yaml | 1 + .../mysql-schema/k8s/operator.yaml | 10 +-- sample-operators/mysql-schema/pom.xml | 7 ++ .../sample/MySQLSchemaReconciler.java | 9 ++- .../sample/MySQLSchemaOperatorE2E.java | 31 +------ sample-operators/tomcat-operator/pom.xml | 7 ++ 10 files changed, 117 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/e2e-test-mysql.yml rename .github/workflows/{end-to-end-tests.yml => e2e-test-tomcat.yml} (96%) diff --git a/.github/workflows/e2e-test-mysql.yml b/.github/workflows/e2e-test-mysql.yml new file mode 100644 index 0000000000..79c36814bf --- /dev/null +++ b/.github/workflows/e2e-test-mysql.yml @@ -0,0 +1,80 @@ +# End to end integration test which deploys the Tomcat operator to a Kubernetes +# (Kind) cluster and creates custom resources to verify the operator's functionality +name: MySQL Schema Operator End to End test +on: + pull_request: + branches: [ main, v1 ] +jobs: + tomcat_e2e_test: + runs-on: ubuntu-latest + env: + KIND_CL_NAME: e2e-test + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: clean resident local docker + if: ${{ env.ACT }} + continue-on-error: true + run: | + for DIMG in "$KIND_CL_NAME-control-plane "; do + docker stop $DIMG ; docker rm $DIMG ; + done ; + sleep 1 + + - name: Create Kubernetes KinD Cluster + uses: container-tools/kind-action@v1.7.0 + with: + cluster_name: e2e-test + registry: false + + - name: Deploy MySQL DB + working-directory: sample-operators/mysql-schema + run: | + kubectl create namespace mysql + kubectl apply -f k8s/mysql-deployment.yaml + kubectl apply -f k8s/mysql-service.yaml + + - name: Set up Java and Maven + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt-hotspot + cache: 'maven' + + - name: Build SDK + run: mvn install -DskipTests + + - name: build jib + working-directory: sample-operators/mysql-schema + run: | + mvn --version + mvn -B package jib:dockerBuild jib:buildTar -Djib-maven-image=mysql-schema-operator -DskipTests + kind load image-archive target/jib-image.tar --name=${{ env.KIND_CL_NAME }} + + - name: Apply CRDs + working-directory: sample-operators/mysql-schema + run: | + kubectl apply -f target/classes/META-INF/fabric8/mysqlschemas.mysql.sample.javaoperatorsdk-v1.yml + + - name: Deploy MySQL Operator + working-directory: sample-operators/mysql-schema + run: | + kubectl apply -f k8s/operator.yaml + + - name: Run E2E Tests + working-directory: sample-operators/mysql-schema + run: mvn -B test -P end-to-end-tests + + - name: Dump state + if: ${{ failure() }} + run: | + set +e + echo "All namespaces" + kubectl get ns + echo "All objects in mysql" + kubectl get all -n mysql-schema-test" -o yaml + echo "Output of mysql pod" + kubectl logs -l app=mysql-schema-operator -n mysql-schema + echo "All objects in mysql-schema-test" + kubectl get deployment,pod,tomcat,webapp -n mysql-schema-test" -o yaml diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/e2e-test-tomcat.yml similarity index 96% rename from .github/workflows/end-to-end-tests.yml rename to .github/workflows/e2e-test-tomcat.yml index 634824c61b..3c2d106790 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/e2e-test-tomcat.yml @@ -1,10 +1,9 @@ # End to end integration test which deploys the Tomcat operator to a Kubernetes # (Kind) cluster and creates custom resources to verify the operator's functionality -name: TomcatOperator End to End test +name: Tomcat Operator End to End test on: - push: - branches: - - "*" + pull_request: + branches: [ main, v1 ] jobs: tomcat_e2e_test: runs-on: ubuntu-latest 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 fc9c158890..5269f75469 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 @@ -91,6 +91,7 @@ public void onResourceDeleted(R resource) { var resourceID = ResourceID.fromResource(resource); TimerTask task = timerTasks.remove(resourceID); if (task != null) { + log.debug("Canceling task for resource: {}", resource); task.cancel(); } cache.remove(resourceID); @@ -100,7 +101,7 @@ private void checkAndRegisterTask(R resource) { var resourceID = ResourceID.fromResource(resource); if (timerTasks.get(resourceID) == null && (registerPredicate == null || registerPredicate.test(resource))) { - timer.schedule(new TimerTask() { + var task = new TimerTask() { @Override public void run() { if (!isRunning()) { @@ -112,7 +113,9 @@ public void run() { res.ifPresentOrElse(r -> pollForResource(r), () -> log.warn("No resource in cache for resource ID: {}", resourceID)); } - }, 0, period); + }; + timerTasks.put(resourceID, task); + timer.schedule(task, 0, period); } } diff --git a/sample-operators/mysql-schema/k8s/mysql-deployment.yaml b/sample-operators/mysql-schema/k8s/mysql-deployment.yaml index 9dfe210212..a257b1b99d 100644 --- a/sample-operators/mysql-schema/k8s/mysql-deployment.yaml +++ b/sample-operators/mysql-schema/k8s/mysql-deployment.yaml @@ -2,6 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: mysql + namespace: mysql spec: selector: matchLabels: diff --git a/sample-operators/mysql-schema/k8s/mysql-service.yaml b/sample-operators/mysql-schema/k8s/mysql-service.yaml index 3b4188373d..4c67148be3 100644 --- a/sample-operators/mysql-schema/k8s/mysql-service.yaml +++ b/sample-operators/mysql-schema/k8s/mysql-service.yaml @@ -2,6 +2,7 @@ apiVersion: v1 kind: Service metadata: name: mysql + namespace: mysql spec: ports: - port: 3306 diff --git a/sample-operators/mysql-schema/k8s/operator.yaml b/sample-operators/mysql-schema/k8s/operator.yaml index f3e667f3ee..f4b3296e09 100644 --- a/sample-operators/mysql-schema/k8s/operator.yaml +++ b/sample-operators/mysql-schema/k8s/operator.yaml @@ -23,8 +23,8 @@ spec: serviceAccountName: mysql-schema-operator # specify the ServiceAccount under which's RBAC persmissions the operator will be executed under containers: - name: operator - image: ${DOCKER_REGISTRY}/mysql-schema-operator:${OPERATOR_VERSION} - imagePullPolicy: Always + image: mysql-schema-operator + imagePullPolicy: IfNotPresent ports: - containerPort: 80 env: @@ -55,7 +55,7 @@ metadata: namespace: mysql-schema-operator --- -apiVersion: rbac.authorization.k8s.io/v1beta1 +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: mysql-schema-operator @@ -63,13 +63,13 @@ rules: - apiGroups: - mysql.sample.javaoperatorsdk resources: - - schemas + - mysqlschemas verbs: - "*" - apiGroups: - mysql.sample.javaoperatorsdk resources: - - schemas/status + - mysqlschemas/status verbs: - "*" - apiGroups: diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index e3fd67c001..c07edc2583 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -71,6 +71,13 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + 0 + + com.google.cloud.tools jib-maven-plugin diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index ce9d062927..23986c7a9b 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -50,9 +50,12 @@ public List prepareEventSources( @Override public UpdateControl reconcile(MySQLSchema schema, Context context) { + log.info("Reconciling MySQLSchema with name: {}", schema.getMetadata().getName()); var dbSchema = context.getSecondaryResource(Schema.class); + log.debug("Schema: {} found for: {} ", dbSchema, schema.getMetadata().getName()); try (Connection connection = getConnection()) { - if (dbSchema == null) { + if (dbSchema.isEmpty()) { + log.debug("Creating Schema and related resources for: {}", schema.getMetadata().getName()); var schemaName = schema.getMetadata().getName(); String password = RandomStringUtils.randomAlphanumeric(16); String secretName = String.format(SECRET_FORMAT, schemaName); @@ -64,8 +67,10 @@ public UpdateControl reconcile(MySQLSchema schema, Context context) updateStatusPojo(schema, secretName, userName); log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); return UpdateControl.updateStatus(schema); + } else { + log.debug("No update on MySQLSchema with name: {}", schema.getMetadata().getName()); + return UpdateControl.noUpdate(); } - return UpdateControl.noUpdate(); } catch (SQLException e) { log.error("Error while creating Schema", e); throw new IllegalStateException(e); 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 75c5412e9c..f4c529426b 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 @@ -1,9 +1,7 @@ package io.javaoperatorsdk.operator.sample; -import java.io.File; import java.io.IOException; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,9 +14,6 @@ import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; - import static java.util.concurrent.TimeUnit.MINUTES; import static org.awaitility.Awaitility.await; import static org.hamcrest.CoreMatchers.equalTo; @@ -26,10 +21,10 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; -@Disabled public class MySQLSchemaOperatorE2E { final static String TEST_NS = "mysql-schema-test"; + final static String MY_SQL_NS = "mysql"; final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); @@ -71,12 +66,7 @@ public void test() throws IOException { log.info("Creating test namespace {}", TEST_NS); client.namespaces().create(testNs); - log.info("Deploying MySQL server"); - deployMySQLServer(client); - log.info("Creating test MySQLSchema object: {}", testSchema); - // var mysqlSchemaClient = client.customResources(MySQLSchema.class); - // mysqlSchemaClient.inNamespace(TEST_NS).createOrReplace(testSchema); client.resource(testSchema).createOrReplace(); log.info("Waiting 5 minutes for expected resources to be created and updated"); @@ -90,23 +80,4 @@ public void test() throws IOException { }); } - private void deployMySQLServer(KubernetesClient client) throws IOException { - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - Deployment deployment = - mapper.readValue(new File("k8s/mysql-deployment.yaml"), Deployment.class); - deployment.getMetadata().setNamespace(TEST_NS); - Service service = mapper.readValue(new File("k8s/mysql-service.yaml"), Service.class); - service.getMetadata().setNamespace(TEST_NS); - client.resource(deployment).createOrReplace(); - client.resource(service).createOrReplace(); - - log.info("Waiting for MySQL server to start"); - await().atMost(5, MINUTES).until(() -> { - Deployment mysqlDeployment = client.apps().deployments().inNamespace(TEST_NS) - .withName(deployment.getMetadata().getName()).get(); - return mysqlDeployment.getStatus().getReadyReplicas() != null - && mysqlDeployment.getStatus().getReadyReplicas() == 1; - }); - } - } diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 85edf9bbaf..cfcfdfb19a 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -61,6 +61,13 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + 0 + + com.google.cloud.tools jib-maven-plugin From a374f46a5cd9df3c3a9880003e8109907c7bc43b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 5 Jan 2022 09:56:09 +0100 Subject: [PATCH 0204/1608] fix: use proper resource name computation (#791) --- .../operator/api/config/ControllerConfiguration.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 94d2e0a8f4..ab4bdb121f 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 @@ -5,7 +5,6 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @@ -18,7 +17,7 @@ default String getName() { } default String getResourceTypeName() { - return CustomResource.getCRDName(getResourceClass()); + return HasMetadata.getFullResourceName(getResourceClass()); } default String getFinalizer() { From 14a543e76579714ea0ff89727d7dd36d5fde3283 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 5 Jan 2022 09:56:57 +0100 Subject: [PATCH 0205/1608] fix: use appropriate terms where needed (#790) --- docs/documentation/features.md | 16 ++++--- docs/documentation/v2-migration.md | 47 ++++++++++--------- .../api/reconciler/DeleteControl.java | 2 +- .../operator/processing/Controller.java | 6 +-- .../event/ReconciliationDispatcher.java | 15 ++++-- .../sample/MySQLSchemaReconciler.java | 2 +- sample-operators/tomcat-operator/README.md | 23 ++++----- .../operator/sample/WebPageReconciler.java | 2 +- .../sample/CustomServiceReconciler.java | 7 ++- 9 files changed, 67 insertions(+), 53 deletions(-) diff --git a/docs/documentation/features.md b/docs/documentation/features.md index b710eeb3e3..dd8be46ecf 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -96,13 +96,15 @@ Always update the custom resource with `UpdateControl`, not with the actual kube On resource updates there is always an optimistic version control in place, to make sure that another update is not overwritten (by setting `resourceVersion` ) . -The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent resource are cleaned -up in `cleanup` implementation. - -However, there is a possibility to not remove the finalizer, this allows to clean up the resources in a more async way, -mostly for the cases when there is a long waiting period after a delete operation is initiated. Note that in this case -you might want to either schedule a timed event to make sure the -`deleteResource` is executed again or use event sources get notified about the state changes of a deleted resource. +The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent +resource are cleaned up in `cleanup` implementation. + +However, there is a possibility to not remove the finalizer, this allows to clean up the resources +in a more async way, mostly for the cases when there is a long waiting period after a delete +operation is initiated. Note that in this case you might want to either schedule a timed event to +make sure +`cleanup` is executed again or use event sources get notified about the state changes of a deleted +resource. ## Generation Awareness and Automatic Observed Generation Handling diff --git a/docs/documentation/v2-migration.md b/docs/documentation/v2-migration.md index fe31fabaf5..4e1ee77fcf 100644 --- a/docs/documentation/v2-migration.md +++ b/docs/documentation/v2-migration.md @@ -1,17 +1,15 @@ --- -title: Migrating from v1 to v2 -description: Migrating from v1 to v2 -layout: docs -permalink: /docs/v2-migration +title: Migrating from v1 to v2 description: Migrating from v1 to v2 layout: docs permalink: +/docs/v2-migration --- # Migrating from v1 to v2 -Version 2 of the framework introduces improvements, features and breaking changes for the APIs both internal and user -facing ones. The migration should be however trivial in most of the cases. For detailed overview of all major issues -until the release of -v`2.0.0` [see milestone on GitHub](https://github.com/java-operator-sdk/java-operator-sdk/milestone/1). For a summary -and reasoning behind some naming changes +Version 2 of the framework introduces improvements, features and breaking changes for the APIs both +internal and user facing ones. The migration should be however trivial in most of the cases. For +detailed overview of all major issues until the release of +v`2.0.0` [see milestone on GitHub](https://github.com/java-operator-sdk/java-operator-sdk/milestone/1) +. For a summary and reasoning behind some naming changes see [this issue](https://github.com/java-operator-sdk/java-operator-sdk/issues/655) ## User Facing API Changes @@ -26,28 +24,35 @@ The following items are renamed and slightly changed: - `deleteResource` renamed to `cleanup` - Events are removed from the [`Context`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java) - of `Reconciler` methods . The rationale behind this, is that there is a consensus now on the pattern that the events - should not be used to implement a reconciliation logic. + of `Reconciler` methods . The rationale behind this, is that there is a consensus now on the + pattern that the events should not be used to implement a reconciliation logic. - The `init` method is extracted from `ResourceController` / `Reconciler` to a separate interface called [EventSourceInitializer](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java) - that `Reconcile` should implement in order to register event sources. See + that `Reconciler` should implement in order to register event sources. The method has been renamed + to `prepareEventSources` and should now return a list of `EventSource` implementations that + the `Controller` will automatically register. See also [sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java) - for usage. Here also - the [`EventSourceManager`](https://github.com/java-operator-sdk/java-operator-sdk/blob/v1/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java) - is renamed - to [`EventSourceRegistry`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceRegistry.java) - , and it's interface refined. + for usage. +- [`EventSourceManager`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java) + is now an internal class that users shouldn't need to interact with. - [`@Controller`](https://github.com/java-operator-sdk/java-operator-sdk/blob/v1/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Controller.java) annotation renamed to [`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java) +- The metrics use `reconcile`, `cleanup` and `resource` labels instead of `createOrUpdate`, `delete` + and `cr`, respectively to match the new logic. ### Event Sources -- Addressing resources within event sources (and in the framework internally) is now changed from `.metadata.uid` to a - pair of `.metadata.name` and optional `.metadata.namespace` of resource. Represented +- Addressing resources within event sources (and in the framework internally) is now changed + from `.metadata.uid` to a pair of `.metadata.name` and optional `.metadata.namespace` of resource. + Represented by [`ResourceID.`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java) -- The [`Event`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java) - API is simplified. Now if an event source produces an event it needs to just produce an instance of this class. +- + +The [`Event`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/Event.java) +API is simplified. Now if an event source produces an event it needs to just produce an instance of +this class. + - [`EventSource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java) is refactored, but the changes are trivial. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java index 5f41192c60..e27698c989 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java @@ -23,7 +23,7 @@ public boolean isRemoveFinalizer() { @Override public DeleteControl rescheduleAfter(long delay) { if (removeFinalizer) { - throw new IllegalStateException("Cannot reschedule deleteResource if removing finalizer"); + throw new IllegalStateException("Cannot reschedule cleanup if removing finalizer"); } return super.rescheduleAfter(delay); } 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 92546b5d1a..317da7ea2f 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 @@ -44,7 +44,7 @@ public DeleteControl cleanup(R resource, Context context) { new ControllerExecution<>() { @Override public String name() { - return "delete"; + return "cleanup"; } @Override @@ -70,7 +70,7 @@ public UpdateControl reconcile(R resource, Context context) { new ControllerExecution<>() { @Override public String name() { - return "createOrUpdate"; + return "reconcile"; } @Override @@ -80,7 +80,7 @@ public String controllerName() { @Override public String successTypeName(UpdateControl result) { - String successType = "cr"; + String successType = "resource"; if (result.isUpdateStatus()) { successType = "status"; } 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 310efaa9dc..cc9ea63920 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 @@ -11,7 +11,14 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.BaseControl; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; @@ -80,8 +87,8 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) * {@link Reconciler#cleanup(HasMetadata, Context)} method * * @param resource the resource to be potentially deleted - * @return {@code true} if the resource should be handed to the controller's {@code - * deleteResource} method, {@code false} otherwise + * @return {@code true} if the resource should be handed to the controller's + * {@link Reconciler#cleanup(HasMetadata, Context)} method, {@code false} otherwise */ private boolean shouldNotDispatchToDelete(R resource) { // we don't dispatch to delete if the controller is configured to use a finalizer but that @@ -133,7 +140,7 @@ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context private PostExecutionControl reconcileExecution(ExecutionScope executionScope, R resourceForExecution, R originalResource, Context context) { log.debug( - "Executing createOrUpdate for resource {} with version: {} with execution scope: {}", + "Reconciling resource {} with version: {} with execution scope: {}", getName(resourceForExecution), getVersion(resourceForExecution), executionScope); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 23986c7a9b..b2fd624b4a 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -79,7 +79,7 @@ public UpdateControl reconcile(MySQLSchema schema, Context context) @Override public DeleteControl cleanup(MySQLSchema schema, Context context) { - log.info("Execution deleteResource for: {}", schema.getMetadata().getName()); + log.info("Cleaning up for: {}", schema.getMetadata().getName()); try (Connection connection = getConnection()) { var dbSchema = SchemaService.getSchema(connection, schema.getMetadata().getName()); if (dbSchema.isPresent()) { diff --git a/sample-operators/tomcat-operator/README.md b/sample-operators/tomcat-operator/README.md index c16335ae80..95168c2196 100644 --- a/sample-operators/tomcat-operator/README.md +++ b/sample-operators/tomcat-operator/README.md @@ -62,15 +62,16 @@ run `kubectl apply -f k8s/operator.yaml`. Now you can create Tomcat instances wi above). ## EventSources -The TomcatController is listening to events about Deployments created by the TomcatOperator by registering a -InformerEventSource with the EventSourceManager. The InformerEventSource will in turn register a watch on -all Deployments managed by the Controller (identified by the `app.kubernetes.io/managed-by` label). -When an event from a Deployment is received we have to identify which Tomcat object does the Deployment -belong to. This is done when the InformerEventSource creates the event. - -The TomcatController has to take care of setting the `app.kubernetes.io/managed-by` label on the Deployment so the -InformerEventSource can watch the right Deployments. -The TomcatController also has to set `ownerReference` on the Deployment so later the InformerEventSource can -identify which Tomcat does the Deployment belong to. This is necessary so the frameowork can call the Controller -`createOrUpdate` method correctly. +The TomcatController is listening to events about Deployments created by the TomcatOperator by +registering a InformerEventSource with the EventSourceManager. The InformerEventSource will in turn +register a watch on all Deployments managed by the Controller (identified by +the `app.kubernetes.io/managed-by` label). When an event from a Deployment is received we have to +identify which Tomcat object does the Deployment belong to. This is done when the +InformerEventSource creates the event. + +The TomcatController has to take care of setting the `app.kubernetes.io/managed-by` label on the +Deployment so the InformerEventSource can watch the right Deployments. The TomcatController also has +to set `ownerReference` on the Deployment so later the InformerEventSource can identify which Tomcat +does the Deployment belong to. This is necessary so the framework can call the Controller +`reconcile` method correctly. diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index ddb353ac05..c252ba6381 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -118,7 +118,7 @@ public UpdateControl reconcile(WebPage webPage, Context context) { @Override public DeleteControl cleanup(WebPage nginx, Context context) { - log.info("Execution deleteResource for: {}", nginx.getMetadata().getName()); + log.info("Cleaning up for: {}", nginx.getMetadata().getName()); log.info("Deleting ConfigMap {}", configMapName(nginx)); Resource configMap = diff --git a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java index 60d859565e..5e3348fadd 100644 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java +++ b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java @@ -17,7 +17,6 @@ @ControllerConfiguration public class CustomServiceReconciler implements Reconciler { - public static final String KIND = "CustomService"; private static final Logger log = LoggerFactory.getLogger(CustomServiceReconciler.class); private final KubernetesClient kubernetesClient; @@ -32,14 +31,14 @@ public CustomServiceReconciler(KubernetesClient kubernetesClient) { @Override public DeleteControl cleanup(CustomService resource, Context context) { - log.info("Execution deleteResource for: {}", resource.getMetadata().getName()); - return DeleteControl.defaultDelete(); + log.info("Cleaning up for: {}", resource.getMetadata().getName()); + return Reconciler.super.cleanup(resource, context); } @Override public UpdateControl reconcile( CustomService resource, Context context) { - log.info("Execution createOrUpdateResource for: {}", resource.getMetadata().getName()); + log.info("Reconciling: {}", resource.getMetadata().getName()); ServicePort servicePort = new ServicePort(); servicePort.setPort(8080); From 26e96c8c80c67012d4d4025c04c3f20c174ca8ef Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 5 Jan 2022 11:28:36 +0100 Subject: [PATCH 0206/1608] fix: make things more resilient in the absence of explicit configuration (#789) --- .../api/config/ExecutorServiceManager.java | 16 ++++- .../operator/processing/Controller.java | 35 +++++++-- .../processing/event/EventProcessor.java | 6 +- .../event/ReconciliationDispatcher.java | 5 +- .../ControllerResourceEventSource.java | 5 +- .../operator/OperatorTest.java | 10 +-- .../event/EventSourceManagerTest.java | 7 +- .../event/ReconciliationDispatcherTest.java | 72 +++++++++++-------- .../event/source/ResourceEventFilterTest.java | 12 +--- .../ControllerResourceEventSourceTest.java | 9 +-- 10 files changed, 104 insertions(+), 73 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index 62d44b66a2..dfe64d97d0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -19,16 +19,23 @@ public class ExecutorServiceManager { private final ExecutorService executor; private final int terminationTimeoutSeconds; - private ExecutorServiceManager(ExecutorService executor, int terminationTimeoutSeconds) { + private ExecutorServiceManager(InstrumentedExecutorService executor, + int terminationTimeoutSeconds) { this.executor = executor; this.terminationTimeoutSeconds = terminationTimeoutSeconds; } public static void init(ConfigurationService configuration) { if (instance == null) { + if (configuration == null) { + configuration = new BaseConfigurationService(Version.UNKNOWN); + } instance = new ExecutorServiceManager( new InstrumentedExecutorService(configuration.getExecutorService()), configuration.getTerminationTimeoutSeconds()); + log.debug("Initialized ExecutorServiceManager executor: {}, timeout: {}", + configuration.getExecutorService().getClass(), + configuration.getTerminationTimeoutSeconds()); } else { log.debug("Already started, reusing already setup instance!"); } @@ -45,8 +52,8 @@ public static void stop() { public static ExecutorServiceManager instance() { if (instance == null) { - throw new IllegalStateException( - "ExecutorServiceManager hasn't been started. Call start method before using!"); + // provide a default configuration if none has been provided by init + init(null); } return instance; } @@ -72,6 +79,9 @@ private static class InstrumentedExecutorService implements ExecutorService { private final ExecutorService executor; private InstrumentedExecutorService(ExecutorService executor) { + if (executor == null) { + throw new NullPointerException(); + } this.executor = executor; debug = Utils.debugThreadPool(); } 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 317da7ea2f..d5e2ae22a0 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 @@ -12,7 +12,11 @@ import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.Version; +import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -29,6 +33,7 @@ public class Controller implements Reconciler, private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; private EventSourceManager eventSourceManager; + private volatile ConfigurationService configurationService; public Controller(Reconciler reconciler, ControllerConfiguration configuration, @@ -40,7 +45,7 @@ public Controller(Reconciler reconciler, @Override public DeleteControl cleanup(R resource, Context context) { - return configuration.getConfigurationService().getMetrics().timeControllerExecution( + return metrics().timeControllerExecution( new ControllerExecution<>() { @Override public String name() { @@ -66,7 +71,7 @@ public DeleteControl execute() { @Override public UpdateControl reconcile(R resource, Context context) { - return configuration.getConfigurationService().getMetrics().timeControllerExecution( + return metrics().timeControllerExecution( new ControllerExecution<>() { @Override public String name() { @@ -97,6 +102,11 @@ public UpdateControl execute() { }); } + private Metrics metrics() { + final var metrics = configurationService().getMetrics(); + return metrics != null ? metrics : Metrics.NOOP; + } + @Override public List prepareEventSources(EventSourceInitializationContext context) { throw new UnsupportedOperationException("This method should never be called directly"); @@ -157,7 +167,7 @@ public void start() throws OperatorException { try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config - if (configuration.getConfigurationService().checkCRDAndValidateLocalModel()) { + if (configurationService().checkCRDAndValidateLocalModel()) { crd = kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName) .get(); @@ -174,7 +184,7 @@ public void start() throws OperatorException { ((EventSourceInitializer) reconciler) .prepareEventSources(new EventSourceInitializationContext<>( eventSourceManager.getControllerResourceEventSource().getResourceCache(), - configuration.getConfigurationService())) + configurationService())) .forEach(eventSourceManager::registerEventSource); } if (failOnMissingCurrentNS()) { @@ -189,6 +199,23 @@ public void start() throws OperatorException { } } + private ConfigurationService configurationService() { + if (configurationService == null) { + configurationService = configuration.getConfigurationService(); + // make sure we always have a default configuration service + if (configurationService == null) { + // we shouldn't need to register the configuration with the default service + configurationService = new BaseConfigurationService(Version.UNKNOWN) { + @Override + public boolean checkCRDAndValidateLocalModel() { + return false; + } + }; + } + } + return configurationService; + } + private void throwMissingCRDException(String crdName, String specVersion, String controllerName) { throw new MissingCRDException( crdName, 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 894cef8298..4973bd18ba 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 @@ -56,8 +56,10 @@ class EventProcessor implements EventHandler, LifecycleAw new ReconciliationDispatcher<>(eventSourceManager.getController()), GenericRetry.fromConfiguration( eventSourceManager.getController().getConfiguration().getRetryConfiguration()), - eventSourceManager.getController().getConfiguration().getConfigurationService() - .getMetrics(), + eventSourceManager.getController().getConfiguration().getConfigurationService() == null + ? Metrics.NOOP + : eventSourceManager.getController().getConfiguration().getConfigurationService() + .getMetrics(), eventSourceManager); } 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 cc9ea63920..52374c5645 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 @@ -10,6 +10,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.BaseControl; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -131,7 +132,9 @@ private PostExecutionControl handleReconcile( private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { if (isErrorStatusHandlerPresent() || shouldUpdateObservedGenerationAutomatically(resource)) { - return configuration().getConfigurationService().getResourceCloner().clone(resource); + final var configurationService = configuration().getConfigurationService(); + return configurationService != null ? configurationService.getResourceCloner().clone(resource) + : ConfigurationService.DEFAULT_CLONER.clone(resource); } else { return resource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index f2710c3bf6..e06d6386a2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -16,6 +16,7 @@ import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; @@ -45,7 +46,9 @@ public class ControllerResourceEventSource public ControllerResourceEventSource(Controller controller) { super(controller.getConfiguration().getResourceClass()); this.controller = controller; - var cloner = controller.getConfiguration().getConfigurationService().getResourceCloner(); + final var configurationService = controller.getConfiguration().getConfigurationService(); + var cloner = configurationService != null ? configurationService.getResourceCloner() + : ConfigurationService.DEFAULT_CLONER; this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); var filters = new ResourceEventFilter[] { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index 28a19d1081..c234a0b9b2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -8,13 +8,13 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class OperatorTest { @@ -22,7 +22,6 @@ class OperatorTest { private final KubernetesClient kubernetesClient = mock(KubernetesClient.class); private final ConfigurationService configurationService = mock(ConfigurationService.class); private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); - private final Operator operator = new Operator(kubernetesClient, configurationService); private final FooReconciler fooReconciler = FooReconciler.create(); @@ -33,16 +32,13 @@ public void shouldRegisterReconcilerToController() { when(configurationService.getConfigurationFor(fooReconciler)).thenReturn(configuration); when(configuration.watchAllNamespaces()).thenReturn(true); when(configuration.getName()).thenReturn("FOO"); - when(configuration.getResourceClass()).thenReturn(FooReconciler.class); + when(configuration.getResourceClass()).thenReturn(FooCustomResource.class); + when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); // when operator.register(fooReconciler); // then - verify(configuration).watchAllNamespaces(); - verify(configuration).getName(); - verify(configuration).getResourceClass(); - assertThat(operator.getControllers().size()).isEqualTo(1); assertThat(operator.getControllers().get(0).getReconciler()).isEqualTo(fooReconciler); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index b3bf230640..e792c7a156 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -9,9 +9,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -174,10 +172,7 @@ private EventSourceManager initManager() { final Controller controller = mock(Controller.class); final ControllerConfiguration configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(HasMetadata.class); - when(configuration.getConfigurationService()).thenReturn(mock(ConfigurationService.class)); when(controller.getConfiguration()).thenReturn(configuration); - ExecutorServiceManager.init(configuration.getConfigurationService()); - var manager = new EventSourceManager(controller); - return manager; + return new EventSourceManager(controller); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 131da0fa95..479fccb5dd 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 @@ -15,8 +15,14 @@ import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.config.RetryConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.CustomResourceFacade; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; @@ -27,7 +33,14 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; class ReconciliationDispatcherTest { @@ -37,8 +50,6 @@ class ReconciliationDispatcherTest { private ReconciliationDispatcher reconciliationDispatcher; private final Reconciler reconciler = mock(Reconciler.class, withSettings().extraInterfaces(ErrorStatusHandler.class)); - private final ControllerConfiguration configuration = - mock(ControllerConfiguration.class); private final ConfigurationService configService = mock(ConfigurationService.class); private final CustomResourceFacade customResourceFacade = mock(ReconciliationDispatcher.CustomResourceFacade.class); @@ -47,27 +58,38 @@ class ReconciliationDispatcherTest { void setup() { testCustomResource = TestUtils.testCustomResource(); reconciliationDispatcher = - init(testCustomResource, reconciler, configuration, customResourceFacade); + init(testCustomResource, reconciler, null, customResourceFacade, true); } private ReconciliationDispatcher init(R customResource, Reconciler reconciler, ControllerConfiguration configuration, - CustomResourceFacade customResourceFacade) { - when(configuration.getFinalizer()).thenReturn(DEFAULT_FINALIZER); + CustomResourceFacade customResourceFacade, boolean useFinalizer) { + configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; + final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER; + when(configuration.getFinalizer()).thenReturn(finalizer); when(configuration.useFinalizer()).thenCallRealMethod(); when(configuration.getName()).thenReturn("EventDispatcherTestController"); - when(configService.getMetrics()).thenReturn(Metrics.NOOP); + when(configuration.getResourceClass()).thenReturn((Class) customResource.getClass()); + when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); when(configuration.getConfigurationService()).thenReturn(configService); + + /* + * We need this for mock reconcilers to properly generate the expected UpdateControl: without + * this, calls such as `when(reconciler.reconcile(eq(testCustomResource), + * any())).thenReturn(UpdateControl.updateStatus(testCustomResource))` will return null because + * equals will fail on the two equal but NOT identical TestCustomResources because equals is not + * implemented on TestCustomResourceSpec or TestCustomResourceStatus + */ when(configService.getResourceCloner()).thenReturn(new Cloner() { @Override + public R clone(R object) { return object; } }); when(reconciler.cleanup(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); - Controller controller = - new Controller<>(reconciler, configuration, null); + Controller controller = new Controller<>(reconciler, configuration, null); return new ReconciliationDispatcher<>(controller, customResourceFacade); } @@ -141,10 +163,11 @@ void callsDeleteIfObjectHasFinalizerAndMarkedForDelete() { @Test void doesNotCallDeleteOnControllerIfMarkedForDeletionWhenNoFinalizerIsConfigured() { - configureToNotUseFinalizer(); + final ReconciliationDispatcher dispatcher = + init(testCustomResource, reconciler, null, customResourceFacade, false); markForDeletion(testCustomResource); - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, times(0)).cleanup(eq(testCustomResource), any()); } @@ -158,23 +181,12 @@ void doNotCallDeleteIfMarkedForDeletionWhenFinalizerHasAlreadyBeenRemoved() { verify(reconciler, never()).cleanup(eq(testCustomResource), any()); } - private void configureToNotUseFinalizer() { - ControllerConfiguration configuration = - mock(ControllerConfiguration.class); - when(configuration.getName()).thenReturn("EventDispatcherTestController"); - when(configService.getMetrics()).thenReturn(Metrics.NOOP); - when(configuration.getConfigurationService()).thenReturn(configService); - when(configuration.useFinalizer()).thenReturn(false); - reconciliationDispatcher = - new ReconciliationDispatcher(new Controller(reconciler, configuration, null), - customResourceFacade); - } - @Test void doesNotAddFinalizerIfConfiguredNotTo() { - configureToNotUseFinalizer(); + final ReconciliationDispatcher dispatcher = + init(testCustomResource, reconciler, null, customResourceFacade, false); - reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + dispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); assertEquals(0, testCustomResource.getMetadata().getFinalizers().size()); } @@ -313,7 +325,7 @@ void setObservedGenerationForStatusIfNeeded() { ControllerConfiguration config = mock(ControllerConfiguration.class); CustomResourceFacade facade = mock(CustomResourceFacade.class); - var dispatcher = init(observedGenResource, reconciler, config, facade); + var dispatcher = init(observedGenResource, reconciler, config, facade, true); when(config.isGenerationAware()).thenReturn(true); when(reconciler.reconcile(any(), any())) @@ -338,7 +350,7 @@ void updatesObservedGenerationOnNoUpdateUpdateControl() { when(reconciler.reconcile(any(), any())) .thenReturn(UpdateControl.noUpdate()); when(facade.updateStatus(observedGenResource)).thenReturn(observedGenResource); - var dispatcher = init(observedGenResource, reconciler, config, facade); + var dispatcher = init(observedGenResource, reconciler, config, facade, true); PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); @@ -359,7 +371,7 @@ void updateObservedGenerationOnCustomResourceUpdate() { .thenReturn(UpdateControl.updateResource(observedGenResource)); when(facade.replaceWithLock(any())).thenReturn(observedGenResource); when(facade.updateStatus(observedGenResource)).thenReturn(observedGenResource); - var dispatcher = init(observedGenResource, reconciler, config, facade); + var dispatcher = init(observedGenResource, reconciler, config, facade, true); PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); 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 913c93d301..f2d6b22909 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 @@ -12,7 +12,6 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; @@ -28,7 +27,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; class ResourceEventFilterTest { public static final String FINALIZER = "finalizer"; @@ -106,8 +104,6 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { @Test public void observedGenerationFiltering() { var config = new ObservedGenControllerConfig(FINALIZER, true, null); - when(config.getConfigurationService().getResourceCloner()) - .thenReturn(ConfigurationService.DEFAULT_CLONER); var eventSource = init(new ObservedGenController(config)); @@ -135,9 +131,6 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { oldResource.getStatus().getConfigMapStatus(), newResource.getStatus().getConfigMapStatus())); - when(config.getConfigurationService().getResourceCloner()) - .thenReturn(ConfigurationService.DEFAULT_CLONER); - final var eventSource = init(new TestController(config)); TestCustomResource cr = TestUtils.testCustomResource(); @@ -184,10 +177,7 @@ public ControllerConfig(String finalizer, boolean generationAware, null, eventFilter, customResourceClass, - mock(ConfigurationService.class)); - - when(getConfigurationService().getResourceCloner()) - .thenReturn(ConfigurationService.DEFAULT_CLONER); + null); } } 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 331daabf36..70b208a194 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 @@ -10,9 +10,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; -import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; @@ -25,7 +23,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; class ControllerResourceEventSourceTest extends AbstractEventSourceTest, EventHandler> { @@ -173,11 +170,7 @@ public TestConfiguration(boolean generationAware) { null, null, TestCustomResource.class, - mock(ConfigurationService.class)); - when(getConfigurationService().getResourceCloner()) - .thenReturn(ConfigurationService.DEFAULT_CLONER); - when(getConfigurationService().getMetrics()) - .thenReturn(Metrics.NOOP); + null); } } } From 073801a82fbdadb408c71bd85175f9b3b67f1cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 5 Jan 2022 11:29:59 +0100 Subject: [PATCH 0207/1608] fix: build e2e tests on main (#792) --- .github/workflows/e2e-test-mysql.yml | 3 +++ .github/workflows/e2e-test-tomcat.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/e2e-test-mysql.yml b/.github/workflows/e2e-test-mysql.yml index 79c36814bf..a106be3779 100644 --- a/.github/workflows/e2e-test-mysql.yml +++ b/.github/workflows/e2e-test-mysql.yml @@ -4,6 +4,9 @@ name: MySQL Schema Operator End to End test on: pull_request: branches: [ main, v1 ] + push: + branches: + - main jobs: tomcat_e2e_test: runs-on: ubuntu-latest diff --git a/.github/workflows/e2e-test-tomcat.yml b/.github/workflows/e2e-test-tomcat.yml index 3c2d106790..70e01660d0 100644 --- a/.github/workflows/e2e-test-tomcat.yml +++ b/.github/workflows/e2e-test-tomcat.yml @@ -4,6 +4,9 @@ name: Tomcat Operator End to End test on: pull_request: branches: [ main, v1 ] + push: + branches: + - main jobs: tomcat_e2e_test: runs-on: ubuntu-latest From 4088a3c113dba5f341f506f97efa59ec4cfcbd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 5 Jan 2022 13:13:37 +0100 Subject: [PATCH 0208/1608] fix: Optional associated resource (#793) --- .../operator/api/reconciler/DefaultContext.java | 6 ++++-- .../processing/event/source/CachingEventSource.java | 4 ++-- .../processing/event/source/ResourceEventSource.java | 4 +++- .../source/controller/ControllerResourceEventSource.java | 4 ++-- .../event/source/informer/InformerEventSource.java | 4 ++-- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 05e185940c..f4b862b9ea 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -4,6 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; public class DefaultContext

implements Context { @@ -24,8 +25,9 @@ public Optional getRetryInfo() { @Override public Optional getSecondaryResource(Class expectedType, String eventSourceName) { - final var eventSource = + final Optional> eventSource = controller.getEventSourceManager().getResourceEventSourceFor(expectedType, eventSourceName); - return eventSource.map(es -> es.getAssociated(primaryResource)); + return eventSource.isEmpty() ? Optional.empty() + : eventSource.get().getAssociated(primaryResource); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index 5e65518282..486ffb81a6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -90,8 +90,8 @@ public void stop() throws OperatorException { } @Override - public T getAssociated(P primary) { - return cache.get(ResourceID.fromResource(primary)).orElse(null); + public Optional getAssociated(P primary) { + return cache.get(ResourceID.fromResource(primary)); } protected static class MapCache implements UpdatableCache { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java index a05f6aebfe..ccc907bb0c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ResourceEventSource.java @@ -1,10 +1,12 @@ package io.javaoperatorsdk.operator.processing.event.source; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.HasMetadata; public interface ResourceEventSource

extends EventSource { Class getResourceClass(); - R getAssociated(P primary); + Optional getAssociated(P primary); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index e06d6386a2..213308b9fe 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -201,7 +201,7 @@ private void handleKubernetesClientException(Exception e) { } @Override - public T getAssociated(T primary) { - return cache.get(ResourceID.fromResource(primary)).orElse(null); + public Optional getAssociated(T primary) { + return cache.get(ResourceID.fromResource(primary)); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 4ac4288384..dadf24aa74 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -138,9 +138,9 @@ private Store getStore() { * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ - public T getAssociated(P resource) { + public Optional getAssociated(P resource) { final var id = associatedWith.associatedSecondaryID(resource); - return get(id).orElse(null); + return get(id); } From 870eb7d811c0c11f9e9386fc939e9654ddf81cce Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 5 Jan 2022 16:28:38 +0100 Subject: [PATCH 0209/1608] refactor: simplify (#795) --- .../operator/api/reconciler/DefaultContext.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index f4b862b9ea..3d924c2753 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -4,7 +4,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; public class DefaultContext

implements Context { @@ -25,9 +24,8 @@ public Optional getRetryInfo() { @Override public Optional getSecondaryResource(Class expectedType, String eventSourceName) { - final Optional> eventSource = - controller.getEventSourceManager().getResourceEventSourceFor(expectedType, eventSourceName); - return eventSource.isEmpty() ? Optional.empty() - : eventSource.get().getAssociated(primaryResource); + return controller.getEventSourceManager() + .getResourceEventSourceFor(expectedType, eventSourceName) + .flatMap(es -> es.getAssociated(primaryResource)); } } From 2a9c4feb8d113e5259782816b89017a7d93a56e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 5 Jan 2022 18:37:08 +0100 Subject: [PATCH 0210/1608] Event source started before events are processed (simple/naive syncing approach) (#794) --- .../processing/event/EventMarker.java | 19 ++- .../processing/event/EventProcessor.java | 116 +++++++++++------- .../processing/event/EventSourceManager.java | 14 ++- .../event/source/informer/Mappers.java | 10 +- .../PerResourcePollingEventSource.java | 12 ++ .../source/polling/PollingEventSource.java | 11 ++ .../processing/event/EventMarkerTest.java | 13 ++ .../processing/event/EventProcessorTest.java | 17 +++ 8 files changed, 162 insertions(+), 50 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventMarker.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventMarker.java index fb44c933e5..a26c9c7829 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventMarker.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventMarker.java @@ -1,6 +1,11 @@ package io.javaoperatorsdk.operator.processing.event; import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static io.javaoperatorsdk.operator.processing.event.EventMarker.EventingState.NO_EVENT_PRESENT; /** * Manages the state of received events. Basically there can be only three distinct states relevant @@ -23,7 +28,7 @@ public enum EventingState { private EventingState getEventingState(ResourceID resourceID) { EventingState actualState = eventingState.get(resourceID); - return actualState == null ? EventingState.NO_EVENT_PRESENT : actualState; + return actualState == null ? NO_EVENT_PRESENT : actualState; } private void setEventingState(ResourceID resourceID, EventingState state) { @@ -46,7 +51,7 @@ public void unMarkEventReceived(ResourceID resourceID) { switch (actualState) { case EVENT_PRESENT: setEventingState(resourceID, - EventingState.NO_EVENT_PRESENT); + NO_EVENT_PRESENT); break; case DELETE_EVENT_PRESENT: throw new IllegalStateException("Cannot unmark delete event."); @@ -72,10 +77,18 @@ public boolean eventPresent(ResourceID resourceID) { public boolean noEventPresent(ResourceID resourceID) { var actualState = getEventingState(resourceID); - return actualState == EventingState.NO_EVENT_PRESENT; + return actualState == NO_EVENT_PRESENT; } public void cleanup(ResourceID resourceID) { eventingState.remove(resourceID); } + + public List resourceIDsWithEventPresent() { + return eventingState.entrySet().stream() + .filter(e -> e.getValue() != NO_EVENT_PRESENT) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + } 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 4973bd18ba..6f90f38c49 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 @@ -58,25 +58,38 @@ class EventProcessor implements EventHandler, LifecycleAw eventSourceManager.getController().getConfiguration().getRetryConfiguration()), eventSourceManager.getController().getConfiguration().getConfigurationService() == null ? Metrics.NOOP - : eventSourceManager.getController().getConfiguration().getConfigurationService() + : eventSourceManager + .getController() + .getConfiguration() + .getConfigurationService() .getMetrics(), eventSourceManager); } - EventProcessor(ReconciliationDispatcher reconciliationDispatcher, + EventProcessor( + ReconciliationDispatcher reconciliationDispatcher, EventSourceManager eventSourceManager, String relatedControllerName, Retry retry) { - this(eventSourceManager.getControllerResourceEventSource().getResourceCache(), null, + this( + eventSourceManager.getControllerResourceEventSource().getResourceCache(), + null, relatedControllerName, - reconciliationDispatcher, retry, null, eventSourceManager); + reconciliationDispatcher, + retry, + null, + eventSourceManager); } - private EventProcessor(Cache cache, ExecutorService executor, + private EventProcessor( + Cache cache, + ExecutorService executor, String relatedControllerName, - ReconciliationDispatcher reconciliationDispatcher, Retry retry, Metrics metrics, + ReconciliationDispatcher reconciliationDispatcher, + Retry retry, + Metrics metrics, EventSourceManager eventSourceManager) { - this.running = true; + this.running = false; this.executor = executor == null ? new ScheduledThreadPoolExecutor( @@ -99,26 +112,31 @@ public void handleEvent(Event event) { lock.lock(); try { log.debug("Received event: {}", event); - if (!this.running) { - log.debug("Skipping event: {} because the event handler is not started", event); - return; - } + final var resourceID = event.getRelatedCustomResourceID(); MDCUtils.addResourceIDInfo(resourceID); metrics.receivedEvent(event); - handleEventMarking(event); - if (!eventMarker.deleteEventPresent(resourceID)) { - submitReconciliationExecution(resourceID); - } else { - cleanupForDeletedEvent(resourceID); + if (!this.running) { + // events are received and marked, but will be processed when started, see start() method. + log.debug("Skipping event: {} because the event processor is not started", event); + return; } + handleMarkedEventForResource(resourceID); } finally { lock.unlock(); MDCUtils.removeResourceIDInfo(); } } + private void handleMarkedEventForResource(ResourceID resourceID) { + if (!eventMarker.deleteEventPresent(resourceID)) { + submitReconciliationExecution(resourceID); + } else { + cleanupForDeletedEvent(resourceID); + } + } + private void submitReconciliationExecution(ResourceID resourceID) { try { boolean controllerUnderExecution = isControllerUnderExecution(resourceID); @@ -148,8 +166,8 @@ private void submitReconciliationExecution(ResourceID resourceID) { } private void handleEventMarking(Event event) { - if (event instanceof ResourceEvent && - ((ResourceEvent) event).getAction() == ResourceAction.DELETED) { + if (event instanceof ResourceEvent + && ((ResourceEvent) event).getAction() == ResourceAction.DELETED) { eventMarker.markDeleteEventReceived(event); } else if (!eventMarker.deleteEventPresent(event.getRelatedCustomResourceID())) { eventMarker.markEventReceived(event); @@ -177,10 +195,11 @@ void eventProcessingFinished( // If a delete event present at this phase, it was received during reconciliation. // So we either removed the finalizer during reconciliation or we don't use finalizers. // Either way we don't want to retry. - if (isRetryConfigured() && postExecutionControl.exceptionDuringExecution() && - !eventMarker.deleteEventPresent(resourceID)) { - handleRetryOnException(executionScope, - postExecutionControl.getRuntimeException().orElseThrow()); + if (isRetryConfigured() + && postExecutionControl.exceptionDuringExecution() + && !eventMarker.deleteEventPresent(resourceID)) { + handleRetryOnException( + executionScope, postExecutionControl.getRuntimeException().orElseThrow()); return; } cleanupOnSuccessfulExecution(executionScope); @@ -195,8 +214,7 @@ void eventProcessingFinished( postponeReconciliationAndHandleCacheSyncEvent(resourceID); } } else { - reScheduleExecutionIfInstructed(postExecutionControl, - executionScope.getResource()); + reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); } } } finally { @@ -208,20 +226,26 @@ private void postponeReconciliationAndHandleCacheSyncEvent(ResourceID resourceID eventSourceManager.getControllerResourceEventSource().whitelistNextEvent(resourceID); } - private boolean isCacheReadyForInstantReconciliation(ExecutionScope executionScope, - PostExecutionControl postExecutionControl) { + private boolean isCacheReadyForInstantReconciliation( + ExecutionScope executionScope, PostExecutionControl postExecutionControl) { if (!postExecutionControl.customResourceUpdatedDuringExecution()) { return true; } String originalResourceVersion = getVersion(executionScope.getResource()); - String customResourceVersionAfterExecution = getVersion(postExecutionControl - .getUpdatedCustomResource() - .orElseThrow(() -> new IllegalStateException( - "Updated custom resource must be present at this point of time"))); - String cachedCustomResourceVersion = getVersion(cache - .get(executionScope.getCustomResourceID()) - .orElseThrow(() -> new IllegalStateException( - "Cached custom resource must be present at this point"))); + String customResourceVersionAfterExecution = + getVersion( + postExecutionControl + .getUpdatedCustomResource() + .orElseThrow( + () -> new IllegalStateException( + "Updated custom resource must be present at this point of time"))); + String cachedCustomResourceVersion = + getVersion( + cache + .get(executionScope.getCustomResourceID()) + .orElseThrow( + () -> new IllegalStateException( + "Cached custom resource must be present at this point"))); if (cachedCustomResourceVersion.equals(customResourceVersionAfterExecution)) { return true; @@ -233,9 +257,10 @@ private boolean isCacheReadyForInstantReconciliation(ExecutionScope execution return !cachedCustomResourceVersion.equals(originalResourceVersion); } - private void reScheduleExecutionIfInstructed(PostExecutionControl postExecutionControl, - R customResource) { - postExecutionControl.getReScheduleDelay() + private void reScheduleExecutionIfInstructed( + PostExecutionControl postExecutionControl, R customResource) { + postExecutionControl + .getReScheduleDelay() .ifPresent(delay -> retryEventSource().scheduleOnce(customResource, delay)); } @@ -248,16 +273,15 @@ TimerEventSource retryEventSource() { * events (received meanwhile retry is in place or already in buffer) instantly or always wait * according to the retry timing if there was an exception. */ - private void handleRetryOnException(ExecutionScope executionScope, - RuntimeException exception) { + private void handleRetryOnException( + ExecutionScope executionScope, RuntimeException exception) { RetryExecution execution = getOrInitRetryExecution(executionScope); var customResourceID = executionScope.getCustomResourceID(); boolean eventPresent = eventMarker.eventPresent(customResourceID); eventMarker.markEventReceived(customResourceID); if (eventPresent) { - log.debug("New events exists for for resource id: {}", - customResourceID); + log.debug("New events exists for for resource id: {}", customResourceID); submitReconciliationExecution(customResourceID); return; } @@ -277,8 +301,7 @@ private void handleRetryOnException(ExecutionScope executionScope, private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) { log.debug( - "Cleanup for successful execution for resource: {}", - getName(executionScope.getResource())); + "Cleanup for successful execution for resource: {}", getName(executionScope.getResource())); if (isRetryConfigured()) { retryState.remove(executionScope.getCustomResourceID()); } @@ -330,11 +353,18 @@ public void start() throws OperatorException { lock.lock(); try { this.running = true; + handleAlreadyMarkedEvents(); } finally { lock.unlock(); } } + private void handleAlreadyMarkedEvents() { + for (ResourceID resourceID : eventMarker.resourceIDsWithEventPresent()) { + handleMarkedEventForResource(resourceID); + } + } + private class ControllerExecution implements Runnable { private final ExecutionScope executionScope; 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 bdd7504d35..f158eccf68 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 @@ -52,9 +52,18 @@ public EventSourceManager(Controller controller) { registerEventSource(controllerEventSource); } + /** + * Starts the event sources first and then the processor. Note that it's not desired to start + * processing events while the event sources are not "synced". This not fully started and the + * caches propagated - although for non k8s related event sources this behavior might be different + * (see + * {@link io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource}). + * + * Now the event sources are also started sequentially, mainly because others might depend on + * {@link ControllerResourceEventSource} , which is started first. + */ @Override - public void start() throws OperatorException { - eventProcessor.start(); + public void start() { lock.lock(); try { log.debug("Starting event sources."); @@ -65,6 +74,7 @@ public void start() throws OperatorException { log.warn("Error starting {} -> {}", eventSource, e); } } + eventProcessor.start(); } finally { lock.unlock(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index 1793275c42..b3e0cf3b65 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -37,10 +37,16 @@ private static PrimaryResourcesRetriever fromMetadata return Collections.emptySet(); } else { final var map = isLabel ? metadata.getLabels() : metadata.getAnnotations(); + if (map == null) { + return Collections.emptySet(); + } + var name = map.get(nameKey); + if (name == null) { + return Collections.emptySet(); + } var namespace = namespaceKey == null ? resource.getMetadata().getNamespace() : map.get(namespaceKey); - return map != null ? Set.of(new ResourceID(map.get(nameKey), namespace)) - : Collections.emptySet(); + return Set.of(new ResourceID(name, namespace)); } }; } 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 5269f75469..0afcb1afae 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 @@ -119,6 +119,18 @@ public void run() { } } + /** + * When this event source is queried for the resource, it might not be fully "synced". Thus, the + * cache might not be propagated, therefore the supplier is checked for the resource too. + * + * @param primary resource of the controller + * @return the related resource for this event source + */ + @Override + public Optional getAssociated(R primary) { + return getValueFromCacheOrSupplier(ResourceID.fromResource(primary)); + } + /** * * @param resourceID of the target related resource 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 7ee7e888e2..8701f31182 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 @@ -53,6 +53,17 @@ public void stop() throws OperatorException { timer.cancel(); } + /** + * See {@link PerResourcePollingEventSource} for more info. + * + * @param primary custom resource + * @return related resource + */ + @Override + public Optional getAssociated(P primary) { + return getValueFromCacheOrSupplier(ResourceID.fromResource(primary)); + } + public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { var resource = getCachedValue(resourceID); if (resource.isPresent()) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventMarkerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventMarkerTest.java index b6fec2b80c..7f808874a8 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventMarkerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventMarkerTest.java @@ -9,6 +9,7 @@ class EventMarkerTest { private final EventMarker eventMarker = new EventMarker(); private ResourceID sampleResourceID = new ResourceID("test-name"); + private ResourceID sampleResourceID2 = new ResourceID("test-name2"); @Test public void returnsNoEventPresentIfNotMarkedYet() { @@ -62,4 +63,16 @@ public void cannotMarkEventAfterDeleteEventReceived() { }); } + @Test + public void listsResourceIDSWithEventsPresent() { + eventMarker.markEventReceived(sampleResourceID); + eventMarker.markEventReceived(sampleResourceID2); + eventMarker.unMarkEventReceived(sampleResourceID); + + var res = eventMarker.resourceIDsWithEventPresent(); + + assertThat(res).hasSize(1); + assertThat(res).contains(sampleResourceID2); + } + } 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 f837f5eea2..01a4be8a79 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 @@ -59,9 +59,11 @@ public void setup() { eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null)); + eventProcessor.start(); eventProcessorWithRetry = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", GenericRetry.defaultLimitedExponentialRetry())); + eventProcessorWithRetry.start(); when(eventProcessor.retryEventSource()).thenReturn(retryTimerEventSourceMock); when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); @@ -270,6 +272,21 @@ public void cancelScheduleOnceEventsOnSuccessfulExecution() { verify(retryTimerEventSourceMock, times(1)).cancelOnceSchedule(eq(crID)); } + @Test + public void startProcessedMarkedEventReceivedBefore() { + var crID = new ResourceID("test-cr", TEST_NAMESPACE); + eventProcessor = + spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null)); + when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(testCustomResource())); + eventProcessor.handleEvent(new Event(crID)); + + verify(reconciliationDispatcherMock, timeout(100).times(0)).handleExecution(any()); + + eventProcessor.start(); + + verify(reconciliationDispatcherMock, timeout(100).times(1)).handleExecution(any()); + } + private ResourceID eventAlreadyUnderProcessing() { when(reconciliationDispatcherMock.handleExecution(any())) .then( From 3ed61ac1fdd01a226ece815b3b4e0acce4e81495 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jan 2022 08:45:19 +0100 Subject: [PATCH 0211/1608] chore(deps): bump kubernetes-client-bom from 5.11.1 to 5.11.2 (#797) Bumps [kubernetes-client-bom](https://github.com/fabric8io/kubernetes-client) from 5.11.1 to 5.11.2. - [Release notes](https://github.com/fabric8io/kubernetes-client/releases) - [Changelog](https://github.com/fabric8io/kubernetes-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/fabric8io/kubernetes-client/compare/v5.11.1...v5.11.2) --- updated-dependencies: - dependency-name: io.fabric8:kubernetes-client-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9c585c837b..2d85db1136 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ ${java.version} 5.8.2 - 5.11.1 + 5.11.2 1.7.32 2.17.1 4.2.0 From 29b05468b5f439473e750a5080428446bf4c1219 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 7 Jan 2022 10:54:00 +0100 Subject: [PATCH 0212/1608] feat: rename context to `EventSourceContext`, provide KubernetesClient access (#796) --- ...onContext.java => EventSourceContext.java} | 22 +++++++++++++++---- .../reconciler/EventSourceInitializer.java | 6 ++--- .../operator/processing/Controller.java | 8 +++---- ...formerEventSourceTestCustomReconciler.java | 4 ++-- .../sample/MySQLSchemaReconciler.java | 2 +- .../operator/sample/TomcatReconciler.java | 4 ++-- .../operator/sample/WebappReconciler.java | 6 ++--- 7 files changed, 32 insertions(+), 20 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/{EventSourceInitializationContext.java => EventSourceContext.java} (66%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java similarity index 66% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index a9b409bff1..0a93d33d40 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializationContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -1,24 +1,27 @@ package io.javaoperatorsdk.operator.api.reconciler; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; /** - * Contextual information made available to prepare event sources. + * Contextual information made available to event sources. * * @param

the type associated with the primary resource that is handled by your reconciler */ -public class EventSourceInitializationContext

{ +public class EventSourceContext

{ private final ResourceCache

primaryCache; private final ConfigurationService configurationService; + private final KubernetesClient client; - public EventSourceInitializationContext(ResourceCache

primaryCache, - ConfigurationService configurationService) { + public EventSourceContext(ResourceCache

primaryCache, + ConfigurationService configurationService, KubernetesClient client) { this.primaryCache = primaryCache; this.configurationService = configurationService; + this.client = client; } /** @@ -42,4 +45,15 @@ public ResourceCache

getPrimaryCache() { public ConfigurationService getConfigurationService() { return configurationService; } + + /** + * Provides access to the {@link KubernetesClient} used by the current + * {@link io.javaoperatorsdk.operator.Operator} instance. + * + * @return the {@link KubernetesClient} used by the current + * {@link io.javaoperatorsdk.operator.Operator} instance. + */ + public KubernetesClient getClient() { + return client; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 3a85326041..45f33037d4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -16,9 +16,9 @@ public interface EventSourceInitializer

{ /** * Prepares a list of {@link EventSource} implementations to be registered by the SDK. * - * @param context a {@link EventSourceInitializationContext} providing access to information - * useful to event sources + * @param context a {@link EventSourceContext} providing access to information useful to event + * sources */ - List prepareEventSources(EventSourceInitializationContext

context); + List prepareEventSources(EventSourceContext

context); } 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 d5e2ae22a0..e291a84803 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 @@ -20,7 +20,7 @@ import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializationContext; +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; @@ -108,7 +108,7 @@ private Metrics metrics() { } @Override - public List prepareEventSources(EventSourceInitializationContext context) { + public List prepareEventSources(EventSourceContext context) { throw new UnsupportedOperationException("This method should never be called directly"); } @@ -182,9 +182,9 @@ public void start() throws OperatorException { eventSourceManager = new EventSourceManager<>(this); if (reconciler instanceof EventSourceInitializer) { ((EventSourceInitializer) reconciler) - .prepareEventSources(new EventSourceInitializationContext<>( + .prepareEventSources(new EventSourceContext<>( eventSourceManager.getControllerResourceEventSource().getResourceCache(), - configurationService())) + configurationService(), kubernetesClient)) .forEach(eventSourceManager::registerEventSource); } if (failOnMissingCurrentNS()) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 647903abc4..28807339df 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -9,7 +9,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializationContext; +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; @@ -39,7 +39,7 @@ public class InformerEventSourceTestCustomReconciler implements @Override public List prepareEventSources( - EventSourceInitializationContext context) { + EventSourceContext context) { return List.of(new InformerEventSource<>(kubernetesClient, ConfigMap.class, Mappers.fromAnnotation(RELATED_RESOURCE_NAME))); } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index b2fd624b4a..af77b6ce97 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -42,7 +42,7 @@ public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig my @Override public List prepareEventSources( - EventSourceInitializationContext context) { + EventSourceContext context) { return List.of(new PerResourcePollingEventSource<>( new SchemaPollingResourceSupplier(mysqlDbConfig), context.getPrimaryCache(), POLL_PERIOD, Schema.class)); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index a4c4272a17..6f91410594 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -18,7 +18,7 @@ import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializationContext; +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; @@ -45,7 +45,7 @@ public TomcatReconciler(KubernetesClient client) { } @Override - public List prepareEventSources(EventSourceInitializationContext context) { + public List prepareEventSources(EventSourceContext context) { SharedIndexInformer deploymentInformer = kubernetesClient.apps().deployments().inAnyNamespace() .withLabel("app.kubernetes.io/managed-by", "tomcat-operator") diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 730ef1ca89..0f785feb02 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -20,7 +20,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializationContext; +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; @@ -28,8 +28,6 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import okhttp3.Response; - @ControllerConfiguration public class WebappReconciler implements Reconciler, EventSourceInitializer { @@ -42,7 +40,7 @@ public WebappReconciler(KubernetesClient kubernetesClient) { } @Override - public List prepareEventSources(EventSourceInitializationContext context) { + public List prepareEventSources(EventSourceContext context) { return List.of(new InformerEventSource<>( kubernetesClient, Tomcat.class, t -> { // To create an event to a related WebApp resource and trigger the reconciliation From 3061d2b807bd613550328c81bad7abe32d105470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 7 Jan 2022 11:01:07 +0100 Subject: [PATCH 0213/1608] feature: stale bot (#798) --- .github/workflows/stale-issues-and-prs.yml | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/stale-issues-and-prs.yml diff --git a/.github/workflows/stale-issues-and-prs.yml b/.github/workflows/stale-issues-and-prs.yml new file mode 100644 index 0000000000..604bc4fcd6 --- /dev/null +++ b/.github/workflows/stale-issues-and-prs.yml @@ -0,0 +1,32 @@ +name: 'Close stale issues and PRs' +on: + workflow_dispatch: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4.1.0 + with: + days-before-issue-stale: 60 + days-before-pr-stale: 60 + days-before-issue-close: 14 + days-before-pr-close: 14 + stale-issue-message: > + This issue is stale because it has been open 60 days with no activity. + Remove stale label or comment or this will be closed in 14 days. + close-issue-message: > + This issue was closed because it has been stalled for 14 days with no activity. + stale-pr-message: > + This PR is stale because it has been open 60 days with no activity. + Remove stale label or comment or this will be closed in 14 days. + close-pr-message: > + This PR was closed because it has been stalled for 10 days with no activity. + stale-issue-label: 'stale' + exempt-issue-labels: 'needs-discussion,help wanted,never stale,feature' + stale-pr-label: 'stale' + exempt-pr-labels: 'never stale' + operations-per-run: 500 + ascending: true \ No newline at end of file From fd6e4937696062b4d10f16a9999b7ca2d91608df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 7 Jan 2022 15:07:29 +0100 Subject: [PATCH 0214/1608] fix: minor improvements (#799) --- .../operator/processing/event/EventSourceManager.java | 6 +++--- .../event/source/controller/ResourceEventFilters.java | 2 -- .../event/source/polling/PerResourcePollingEventSource.java | 4 ++++ .../operator/sample/MySQLSchemaOperator.java | 2 ++ .../io/javaoperatorsdk/operator/sample/TomcatOperator.java | 1 + .../io/javaoperatorsdk/operator/sample/WebPageOperator.java | 1 + 6 files changed, 11 insertions(+), 5 deletions(-) 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 f158eccf68..55f42cd438 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 @@ -40,7 +40,7 @@ public class EventSourceManager implements LifecycleAware EventSourceManager(EventProcessor eventProcessor) { this.eventProcessor = eventProcessor; controller = null; - registerEventSource(eventSources.initRetryEventSource()); + registerEventSource(eventSources.retryEventSource()); } public EventSourceManager(Controller controller) { @@ -48,7 +48,7 @@ public EventSourceManager(Controller controller) { // controller event source needs to be available before we create the event processor final var controllerEventSource = eventSources.initControllerEventSource(controller); this.eventProcessor = new EventProcessor<>(this); - registerEventSource(eventSources.initRetryEventSource()); + registerEventSource(eventSources.retryEventSource()); registerEventSource(controllerEventSource); } @@ -184,7 +184,7 @@ ControllerResourceEventSource initControllerEventSource(Controller control return controllerResourceEventSource; } - TimerEventSource initRetryEventSource() { + TimerEventSource retryEventSource() { return retryAndRescheduleTimerEventSource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java index 43fe410fbc..d287361c2b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java @@ -25,8 +25,6 @@ public final class ResourceEventFilters { private static final ResourceEventFilter GENERATION_AWARE = (configuration, oldResource, newResource) -> { final var generationAware = configuration.isGenerationAware(); - // todo: change this to check for HasStatus (or similar) when - // https://github.com/fabric8io/kubernetes-client/issues/3586 is fixed if (newResource instanceof CustomResource) { var newCustomResource = (CustomResource) newResource; final var status = newCustomResource.getStatus(); 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 0afcb1afae..465b3c617a 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 @@ -97,6 +97,10 @@ public void onResourceDeleted(R resource) { cache.remove(resourceID); } + // This method is always called from the same Thread for the same resource, + // since events from ResourceEventAware are propagated from the thread of the informer. This is + // important + // because otherwise there will be a race condition related to the timerTasks. private void checkAndRegisterTask(R resource) { var resourceID = ResourceID.fromResource(resource); if (timerTasks.get(resourceID) == null && (registerPredicate == null 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 e8f25c84bd..fbb9ae2b65 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 @@ -27,8 +27,10 @@ public static void main(String[] args) throws IOException { KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, DefaultConfigurationService.instance()); operator.register(new MySQLSchemaReconciler(client, MySQLDbConfig.loadFromEnvironmentVars())); + operator.installShutdownHook(); operator.start(); + new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); } } 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 a7a7ca40ff..487183dfe5 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 @@ -27,6 +27,7 @@ public static void main(String[] args) throws IOException { Operator operator = new Operator(client, DefaultConfigurationService.instance()); operator.register(new TomcatReconciler(client)); 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/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageOperator.java index 4940119ba9..768fd26a72 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 @@ -27,6 +27,7 @@ public static void main(String[] args) throws IOException { KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, DefaultConfigurationService.instance()); operator.register(new WebPageReconciler(client)); + operator.installShutdownHook(); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD!")), 8080).start(Exit.NEVER); From 9c3c96e625c86d55cfe0d6e5248cac5de95c3fe3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 7 Jan 2022 14:19:23 +0000 Subject: [PATCH 0215/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 2866f42257..fc055ea220 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 47fc3f9881..54409af136 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 7a9977ded0..0339129c75 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 2fe67f8716..0c05632099 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 2d85db1136..77bb042090 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index c07edc2583..3b69bde3c2 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index cb47a55049..d186916abd 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index cfcfdfb19a..913728beca 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 34286abadc..7a6c711df5 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index ed1884aecd..0038730ea3 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 566fbd98be..2d1ad2923a 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index d1a2de4c2b..9fb7f1c92c 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index ce82ac6b2d..27e5c01d70 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 80bed1daf33e8d8cb0cde0f11b437ab486741bef Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 7 Jan 2022 15:29:27 +0100 Subject: [PATCH 0216/1608] fix: version set to 2.0.0-SNAPSHOT --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index fc055ea220..2866f42257 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 54409af136..47fc3f9881 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 0339129c75..7a9977ded0 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 0c05632099..2fe67f8716 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 77bb042090..2d85db1136 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 3b69bde3c2..c07edc2583 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index d186916abd..cb47a55049 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 913728beca..cfcfdfb19a 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 7a6c711df5..34286abadc 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 0038730ea3..ed1884aecd 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 2d1ad2923a..566fbd98be 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 9fb7f1c92c..d1a2de4c2b 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index 27e5c01d70..ce82ac6b2d 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 6277c1c51d130accf9e52fa3ee55e44f667c6e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 7 Jan 2022 16:33:43 +0100 Subject: [PATCH 0217/1608] docs: migration guide render fix (#800) --- docs/documentation/v2-migration.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/documentation/v2-migration.md b/docs/documentation/v2-migration.md index 4e1ee77fcf..4500672308 100644 --- a/docs/documentation/v2-migration.md +++ b/docs/documentation/v2-migration.md @@ -1,6 +1,8 @@ --- -title: Migrating from v1 to v2 description: Migrating from v1 to v2 layout: docs permalink: -/docs/v2-migration +title: Migrating from v1 to v2 +description: Migrating from v1 to v2 +layout: docs +permalink: /docs/v2-migration --- # Migrating from v1 to v2 From 59be5c49633b2ef263d7df6c98c42f924af12c36 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Sat, 8 Jan 2022 17:17:21 +0100 Subject: [PATCH 0218/1608] fix: minor fixes (#802) * fix: e2e test name * fix: missing javadoc * fix: typo + javadoc * fix: simplify assertion * fix: avoid type masking --- .github/workflows/e2e-test-mysql.yml | 22 +++++++++---------- .../api/config/ControllerConfiguration.java | 2 +- .../api/reconciler/ErrorStatusHandler.java | 2 +- .../event/EventSourceManagerTest.java | 8 +++---- .../event/ReconciliationDispatcherTest.java | 2 +- .../runtime/AccumulativeMappingWriter.java | 4 ++-- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/e2e-test-mysql.yml b/.github/workflows/e2e-test-mysql.yml index a106be3779..f906fafc21 100644 --- a/.github/workflows/e2e-test-mysql.yml +++ b/.github/workflows/e2e-test-mysql.yml @@ -7,18 +7,18 @@ on: push: branches: - main -jobs: - tomcat_e2e_test: - runs-on: ubuntu-latest - env: - KIND_CL_NAME: e2e-test - steps: - - name: Checkout - uses: actions/checkout@v2 +jobs: + mysql_e2e_test: + runs-on: ubuntu-latest + env: + KIND_CL_NAME: e2e-test + steps: + - name: Checkout + uses: actions/checkout@v2 - - name: clean resident local docker - if: ${{ env.ACT }} - continue-on-error: true + - name: clean resident local docker + if: ${{ env.ACT }} + continue-on-error: true run: | for DIMG in "$KIND_CL_NAME-control-plane "; do docker stop $DIMG ; docker rm $DIMG ; 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 ab4bdb121f..1353e6ea38 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 @@ -106,7 +106,7 @@ default boolean useFinalizer() { /** * Allow controllers to filter events before they are provided to the * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. Note that the provided - * filter is combined with {@link #isGenerationAware()} to compute the final set of fiolters that + * filter is combined with {@link #isGenerationAware()} to compute the final set of filters that * should be applied; * * @return filter diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java index 7a48f6f661..22a16e4ccd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java @@ -21,7 +21,7 @@ public interface ErrorStatusHandler { * should not be updates on custom resource after it is marked for deletion. * * @param resource to update the status on - * @param retryInfo + * @param retryInfo the current retry status * @param e exception thrown from the reconciler * @return the updated resource */ diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index e792c7a156..a330b4130c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -133,10 +133,10 @@ void retrievingAnEventSourceWhenMultipleAreRegisteredForATypeShouldRequireAQuali assertTrue(exception.getMessage().contains("name1")); assertTrue(exception.getMessage().contains("name2")); - assertTrue(manager.getResourceEventSourceFor(TestCustomResource.class, "name2").get() - .equals(eventSource2)); - assertTrue(manager.getResourceEventSourceFor(TestCustomResource.class, "name1").get() - .equals(eventSource)); + assertEquals(manager.getResourceEventSourceFor(TestCustomResource.class, "name2").get(), + eventSource2); + assertEquals(manager.getResourceEventSourceFor(TestCustomResource.class, "name1").get(), + eventSource); } @Test 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 479fccb5dd..5e642a94b5 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 @@ -83,7 +83,7 @@ private ReconciliationDispatcher init(R customResourc when(configService.getResourceCloner()).thenReturn(new Cloner() { @Override - public R clone(R object) { + public T clone(T object) { return object; } }); diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java index 67cc71ac5a..d0302e2099 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java @@ -55,8 +55,8 @@ public AccumulativeMappingWriter add(String key, String value) { } /** - * Generates or overrise the resource file with the given path - * ({@linkAccumulativeMappingWriter#resourcePath}) + * Generates or override the resource file with the given path + * ({@link AccumulativeMappingWriter#resourcePath}) */ public void flush() { PrintWriter printWriter = null; From f15dc5e84b0acc03bcb0ea2964926e76bb88b9e5 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Sat, 8 Jan 2022 17:40:01 +0100 Subject: [PATCH 0219/1608] fix: wrong indentation (#803) --- .github/workflows/e2e-test-mysql.yml | 106 +++++++++++++-------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/.github/workflows/e2e-test-mysql.yml b/.github/workflows/e2e-test-mysql.yml index f906fafc21..07fe6e3189 100644 --- a/.github/workflows/e2e-test-mysql.yml +++ b/.github/workflows/e2e-test-mysql.yml @@ -19,65 +19,65 @@ jobs: - name: clean resident local docker if: ${{ env.ACT }} continue-on-error: true - run: | - for DIMG in "$KIND_CL_NAME-control-plane "; do - docker stop $DIMG ; docker rm $DIMG ; - done ; - sleep 1 + run: | + for DIMG in "$KIND_CL_NAME-control-plane "; do + docker stop $DIMG ; docker rm $DIMG ; + done ; + sleep 1 - - name: Create Kubernetes KinD Cluster - uses: container-tools/kind-action@v1.7.0 - with: - cluster_name: e2e-test - registry: false + - name: Create Kubernetes KinD Cluster + uses: container-tools/kind-action@v1.7.0 + with: + cluster_name: e2e-test + registry: false - - name: Deploy MySQL DB - working-directory: sample-operators/mysql-schema - run: | - kubectl create namespace mysql - kubectl apply -f k8s/mysql-deployment.yaml - kubectl apply -f k8s/mysql-service.yaml + - name: Deploy MySQL DB + working-directory: sample-operators/mysql-schema + run: | + kubectl create namespace mysql + kubectl apply -f k8s/mysql-deployment.yaml + kubectl apply -f k8s/mysql-service.yaml - - name: Set up Java and Maven - uses: actions/setup-java@v2 - with: - java-version: 11 - distribution: adopt-hotspot - cache: 'maven' + - name: Set up Java and Maven + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: adopt-hotspot + cache: 'maven' - - name: Build SDK - run: mvn install -DskipTests + - name: Build SDK + run: mvn install -DskipTests - - name: build jib - working-directory: sample-operators/mysql-schema - run: | - mvn --version - mvn -B package jib:dockerBuild jib:buildTar -Djib-maven-image=mysql-schema-operator -DskipTests - kind load image-archive target/jib-image.tar --name=${{ env.KIND_CL_NAME }} + - name: build jib + working-directory: sample-operators/mysql-schema + run: | + mvn --version + mvn -B package jib:dockerBuild jib:buildTar -Djib-maven-image=mysql-schema-operator -DskipTests + kind load image-archive target/jib-image.tar --name=${{ env.KIND_CL_NAME }} - - name: Apply CRDs - working-directory: sample-operators/mysql-schema - run: | - kubectl apply -f target/classes/META-INF/fabric8/mysqlschemas.mysql.sample.javaoperatorsdk-v1.yml + - name: Apply CRDs + working-directory: sample-operators/mysql-schema + run: | + kubectl apply -f target/classes/META-INF/fabric8/mysqlschemas.mysql.sample.javaoperatorsdk-v1.yml - - name: Deploy MySQL Operator - working-directory: sample-operators/mysql-schema - run: | - kubectl apply -f k8s/operator.yaml + - name: Deploy MySQL Operator + working-directory: sample-operators/mysql-schema + run: | + kubectl apply -f k8s/operator.yaml - - name: Run E2E Tests - working-directory: sample-operators/mysql-schema - run: mvn -B test -P end-to-end-tests + - name: Run E2E Tests + working-directory: sample-operators/mysql-schema + run: mvn -B test -P end-to-end-tests - - name: Dump state - if: ${{ failure() }} - run: | - set +e - echo "All namespaces" - kubectl get ns - echo "All objects in mysql" - kubectl get all -n mysql-schema-test" -o yaml - echo "Output of mysql pod" - kubectl logs -l app=mysql-schema-operator -n mysql-schema - echo "All objects in mysql-schema-test" - kubectl get deployment,pod,tomcat,webapp -n mysql-schema-test" -o yaml + - name: Dump state + if: ${{ failure() }} + run: | + set +e + echo "All namespaces" + kubectl get ns + echo "All objects in mysql" + kubectl get all -n mysql-schema-test" -o yaml + echo "Output of mysql pod" + kubectl logs -l app=mysql-schema-operator -n mysql-schema + echo "All objects in mysql-schema-test" + kubectl get deployment,pod,tomcat,webapp -n mysql-schema-test" -o yaml From e3930e60a671b6b11c1d07a619760a71a0f3d86b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 08:56:00 +0100 Subject: [PATCH 0220/1608] chore(deps): bump maven-jar-plugin from 3.2.0 to 3.2.1 (#804) Bumps [maven-jar-plugin](https://github.com/apache/maven-jar-plugin) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/apache/maven-jar-plugin/releases) - [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.2.0...maven-jar-plugin-3.2.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-jar-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2d85db1136..f8227a4f65 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 3.3.1 3.2.0 3.2.1 - 3.2.0 + 3.2.1 3.1.0 3.0.1 1.6.8 From d08e54bb3776be3c4ea75f35712dde22c004ab86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 10 Jan 2022 12:47:04 +0100 Subject: [PATCH 0221/1608] fix: naming fixes (#805) --- .../java/io/javaoperatorsdk/operator/Operator.java | 2 +- .../polling/PerResourcePollingEventSource.java | 6 +++--- .../polling/PerResourcePollingEventSourceTest.java | 14 +++++++------- .../sample/SchemaPollingResourceSupplier.java | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) 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 415e5c3505..4b5058ca8a 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 @@ -167,7 +167,7 @@ public synchronized void shouldStart() { return; } if (controllers.isEmpty()) { - throw new OperatorException("No ResourceController exists. Exiting!"); + throw new OperatorException("No Controller exists. Exiting!"); } } 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 465b3c617a..9130d27a45 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 @@ -57,7 +57,7 @@ public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, } private void pollForResource(R resource) { - var value = resourceSupplier.getResources(resource); + var value = resourceSupplier.getResource(resource); var resourceID = ResourceID.fromResource(resource); if (value.isEmpty()) { super.handleDelete(resourceID); @@ -69,7 +69,7 @@ private void pollForResource(R resource) { private Optional getAndCacheResource(ResourceID resourceID) { var resource = resourceCache.get(resourceID); if (resource.isPresent()) { - var value = resourceSupplier.getResources(resource.get()); + var value = resourceSupplier.getResource(resource.get()); value.ifPresent(v -> cache.put(resourceID, v)); return value; } @@ -152,7 +152,7 @@ public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { } public interface ResourceSupplier { - Optional getResources(R resource); + Optional getResource(R resource); } @Override diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java index 43f0a6a5ac..eea42f8f69 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java @@ -35,7 +35,7 @@ class PerResourcePollingEventSourceTest extends @BeforeEach public void setup() { when(resourceCache.get(any())).thenReturn(Optional.of(testCustomResource)); - when(supplier.getResources(any())) + when(supplier.getResource(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())); setUpSource(new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, @@ -47,7 +47,7 @@ public void pollsTheResourceAfterAwareOfIt() throws InterruptedException { source.onResourceCreated(testCustomResource); Thread.sleep(3 * PERIOD); - verify(supplier, atLeast(2)).getResources(eq(testCustomResource)); + verify(supplier, atLeast(2)).getResource(eq(testCustomResource)); verify(eventHandler, times(1)).handleEvent(any()); } @@ -59,31 +59,31 @@ public void registeringTaskOnAPredicate() throws InterruptedException { source.onResourceCreated(testCustomResource); Thread.sleep(2 * PERIOD); - verify(supplier, times(0)).getResources(eq(testCustomResource)); + verify(supplier, times(0)).getResource(eq(testCustomResource)); testCustomResource.getMetadata().setGeneration(2L); source.onResourceUpdated(testCustomResource, testCustomResource); Thread.sleep(2 * PERIOD); - verify(supplier, atLeast(1)).getResources(eq(testCustomResource)); + verify(supplier, atLeast(1)).getResource(eq(testCustomResource)); } @Test public void propagateEventOnDeletedResource() throws InterruptedException { source.onResourceCreated(testCustomResource); - when(supplier.getResources(any())) + when(supplier.getResource(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())) .thenReturn(Optional.empty()); Thread.sleep(3 * PERIOD); - verify(supplier, atLeast(2)).getResources(eq(testCustomResource)); + verify(supplier, atLeast(2)).getResource(eq(testCustomResource)); verify(eventHandler, times(2)).handleEvent(any()); } @Test public void getsValueFromCacheOrSupplier() throws InterruptedException { source.onResourceCreated(testCustomResource); - when(supplier.getResources(any())) + when(supplier.getResource(any())) .thenReturn(Optional.empty()) .thenReturn(Optional.of(SampleExternalResource.testResource1())); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java index 52f3f301c8..e7be1b3c42 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java @@ -16,7 +16,7 @@ public SchemaPollingResourceSupplier(MySQLDbConfig mySQLDbConfig) { } @Override - public Optional getResources(MySQLSchema resource) { + public Optional getResource(MySQLSchema resource) { return schemaService.getSchema(resource.getMetadata().getName()); } } From 9ae6108e67a3b404f61ecfbd4e75e71b4fd42583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 11 Jan 2022 16:53:10 +0100 Subject: [PATCH 0222/1608] fix with resource filter bug and related behavior update (#808) --- .../reconciler/ControllerConfiguration.java | 2 +- .../ControllerResourceEventSource.java | 10 ++-- .../event/source/ResourceEventFilterTest.java | 10 +--- .../runtime/AnnotationConfiguration.java | 2 +- .../operator/CustomResourceFilterIT.java | 51 +++++++++++++++++++ .../CustomFilteringTestReconciler.java | 25 +++++++++ .../CustomFilteringTestResource.java | 15 ++++++ .../CustomFilteringTestResourceSpec.java | 26 ++++++++++ .../sample/customfilter/CustomFlagFilter.java | 13 +++++ .../customfilter/CustomFlagFilter2.java | 13 +++++ .../retry/RetryTestCustomReconciler.java | 7 +-- .../operator/sample/CustomFilter.java | 13 +++++ .../operator/sample/WebappReconciler.java | 2 +- 13 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestResourceSpec.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/CustomFilter.java 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 f72354cf93..fad4bc8573 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 @@ -55,5 +55,5 @@ * @return the list of event filters. */ @SuppressWarnings("rawtypes") - Class[] eventFilters() default {}; + Class[] eventFilters() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 213308b9fe..9feabf40bf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -54,9 +54,7 @@ public ControllerResourceEventSource(Controller controller) { var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), ResourceEventFilters.markedForDeletion(), - ResourceEventFilters.and( - controller.getConfiguration().getEventFilter(), - ResourceEventFilters.generationAware()), + ResourceEventFilters.generationAware(), null }; @@ -66,7 +64,11 @@ public ControllerResourceEventSource(Controller controller) { } else { onceWhitelistEventFilterEventFilter = null; } - filter = ResourceEventFilters.or(filters); + if (controller.getConfiguration().getEventFilter() != null) { + filter = controller.getConfiguration().getEventFilter().and(ResourceEventFilters.or(filters)); + } else { + filter = ResourceEventFilters.or(filters); + } } @Override 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 f2d6b22909..17168876a5 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 @@ -123,7 +123,7 @@ public void observedGenerationFiltering() { } @Test - public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { + public void eventAlwaysFilteredByCustomPredicate() { var config = new TestControllerConfig( FINALIZER, false, @@ -138,13 +138,7 @@ public void eventNotFilteredByCustomPredicateIfFinalizerIsRequired() { cr.getStatus().setConfigMapStatus("1"); eventSource.eventReceived(ResourceAction.UPDATED, cr, cr); - verify(eventHandler, times(1)).handleEvent(any()); - - cr.getMetadata().setGeneration(1L); - cr.getStatus().setConfigMapStatus("1"); - - eventSource.eventReceived(ResourceAction.UPDATED, cr, cr); - verify(eventHandler, times(2)).handleEvent(any()); + verify(eventHandler, times(0)).handleEvent(any()); } private static class TestControllerConfig extends ControllerConfig { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 9a5577a90d..7bda61874c 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -90,7 +90,7 @@ public ResourceEventFilter getEventFilter() { if (answer == null) { answer = filter; } else { - answer = filter.and(filter); + answer = answer.and(filter); } } catch (Exception e) { throw new RuntimeException(e); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java new file mode 100644 index 0000000000..876710ff36 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java @@ -0,0 +1,51 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.customfilter.CustomFilteringTestReconciler; +import io.javaoperatorsdk.operator.sample.customfilter.CustomFilteringTestResource; +import io.javaoperatorsdk.operator.sample.customfilter.CustomFilteringTestResourceSpec; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CustomResourceFilterIT { + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new CustomFilteringTestReconciler()) + .build(); + + @Test + public void doesCustomFiltering() throws InterruptedException { + var filtered1 = createTestResource("filtered1", true, false); + var filtered2 = createTestResource("filtered2", false, true); + var notFiltered = createTestResource("notfiltered", true, true); + operator.create(CustomFilteringTestResource.class, filtered1); + operator.create(CustomFilteringTestResource.class, filtered2); + operator.create(CustomFilteringTestResource.class, notFiltered); + + Thread.sleep(300); + + assertThat( + ((CustomFilteringTestReconciler) operator.getReconcilers().get(0)).getNumberOfExecutions()) + .isEqualTo(1); + } + + + CustomFilteringTestResource createTestResource(String name, boolean filter1, boolean filter2) { + CustomFilteringTestResource resource = new CustomFilteringTestResource(); + resource.setMetadata(new ObjectMeta()); + resource.getMetadata().setName(name); + resource.setSpec(new CustomFilteringTestResourceSpec()); + resource.getSpec().setFilter1(filter1); + resource.getSpec().setFilter2(filter2); + return resource; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java new file mode 100644 index 0000000000..2a13dc3092 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java @@ -0,0 +1,25 @@ +package io.javaoperatorsdk.operator.sample.customfilter; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; + +@ControllerConfiguration(eventFilters = {CustomFlagFilter.class, CustomFlagFilter2.class}) +public class CustomFilteringTestReconciler implements Reconciler { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile(CustomFilteringTestResource resource, + Context context) { + numberOfExecutions.incrementAndGet(); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestResource.java new file mode 100644 index 0000000000..dec7b6c40a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.customfilter; + +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("cft") +public class CustomFilteringTestResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestResourceSpec.java new file mode 100644 index 0000000000..8bb1f48054 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestResourceSpec.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.sample.customfilter; + +public class CustomFilteringTestResourceSpec { + + private boolean filter1; + + private boolean filter2; + + public boolean isFilter1() { + return filter1; + } + + public CustomFilteringTestResourceSpec setFilter1(boolean filter1) { + this.filter1 = filter1; + return this; + } + + public boolean isFilter2() { + return filter2; + } + + public CustomFilteringTestResourceSpec setFilter2(boolean filter2) { + this.filter2 = filter2; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java new file mode 100644 index 0000000000..58d2ed1ede --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.sample.customfilter; + +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; + +public class CustomFlagFilter implements ResourceEventFilter { + + @Override + public boolean acceptChange(ControllerConfiguration configuration, + CustomFilteringTestResource oldResource, CustomFilteringTestResource newResource) { + return newResource.getSpec().isFilter1(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java new file mode 100644 index 0000000000..f38a8c9553 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.sample.customfilter; + +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; + +public class CustomFlagFilter2 implements ResourceEventFilter { + + @Override + public boolean acceptChange(ControllerConfiguration configuration, + CustomFilteringTestResource oldResource, CustomFilteringTestResource newResource) { + return newResource.getSpec().isFilter2(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index 0de3c8c4f2..df26745def 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -24,10 +24,11 @@ public class RetryTestCustomReconciler LoggerFactory.getLogger(RetryTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); - private int numberOfExecutionFails; + private final AtomicInteger numberOfExecutionFails; + public RetryTestCustomReconciler(int numberOfExecutionFails) { - this.numberOfExecutionFails = numberOfExecutionFails; + this.numberOfExecutionFails = new AtomicInteger(numberOfExecutionFails); } @Override @@ -40,7 +41,7 @@ public UpdateControl reconcile(RetryTestCustomResource } log.info("Value: " + resource.getSpec().getValue()); - if (numberOfExecutions.get() < numberOfExecutionFails + 1) { + if (numberOfExecutions.get() < numberOfExecutionFails.get() + 1) { throw new RuntimeException("Testing Retry"); } if (context.getRetryInfo().isEmpty() || context.getRetryInfo().get().isLastAttempt()) { diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/CustomFilter.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/CustomFilter.java new file mode 100644 index 0000000000..4b8ec620b7 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/CustomFilter.java @@ -0,0 +1,13 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; + +public class CustomFilter implements ResourceEventFilter { + @Override + public boolean acceptChange(ControllerConfiguration configuration, HasMetadata oldResource, + HasMetadata newResource) { + return false; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 0f785feb02..66e9a68def 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -28,7 +28,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -@ControllerConfiguration +@ControllerConfiguration(eventFilters = {CustomFilter.class}) public class WebappReconciler implements Reconciler, EventSourceInitializer { private KubernetesClient kubernetesClient; From 339d0304f17ba15a75c0a75e2176c956cecb18b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 12 Jan 2022 14:07:06 +0100 Subject: [PATCH 0223/1608] feature: flag to allow to not close k8s client on stop (#809) --- .../java/io/javaoperatorsdk/operator/Operator.java | 12 +++++++++++- .../operator/api/config/ConfigurationService.java | 4 ++++ .../api/config/ConfigurationServiceOverrider.java | 12 ++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) 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 4b5058ca8a..f8da4ee8a8 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 @@ -27,10 +27,18 @@ public class Operator implements AutoCloseable, LifecycleAware { private final ConfigurationService configurationService; private final ControllerManager controllers = new ControllerManager(); + public Operator(ConfigurationService configurationService) { this(new DefaultKubernetesClient(), configurationService); } + /** + * Note that Operator by default closes the client on stop, this can be changed using + * {@link ConfigurationService} + * + * @param kubernetesClient client to use to all Kubernetes related operations + * @param configurationService provides configuration + */ public Operator(KubernetesClient kubernetesClient, ConfigurationService configurationService) { this.kubernetesClient = kubernetesClient; this.configurationService = configurationService; @@ -97,7 +105,9 @@ public void stop() throws OperatorException { controllers.stop(); ExecutorServiceManager.stop(); - kubernetesClient.close(); + if (configurationService.closeClientOnStop()) { + kubernetesClient.close(); + } } /** Stop the operator. */ 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 2e68d7ff62..2e4679efd3 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 @@ -118,4 +118,8 @@ default Metrics getMetrics() { default ExecutorService getExecutorService() { return Executors.newFixedThreadPool(concurrentReconciliationThreads()); } + + default boolean closeClientOnStop() { + return true; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index 17296fce59..b96f510d97 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -15,6 +15,7 @@ public class ConfigurationServiceOverrider { private int threadNumber; private Cloner cloner; private int timeoutSeconds; + private boolean closeClientOnStop; public ConfigurationServiceOverrider( ConfigurationService original) { @@ -25,6 +26,7 @@ public ConfigurationServiceOverrider( this.cloner = original.getResourceCloner(); this.timeoutSeconds = original.getTerminationTimeoutSeconds(); this.metrics = original.getMetrics(); + this.closeClientOnStop = original.closeClientOnStop(); } @@ -58,6 +60,11 @@ public ConfigurationServiceOverrider withMetrics(Metrics metrics) { return this; } + public ConfigurationServiceOverrider withCloseClientOnStop(boolean close) { + this.closeClientOnStop = close; + return this; + } + public ConfigurationService build() { return new ConfigurationService() { @Override @@ -105,6 +112,11 @@ public int getTerminationTimeoutSeconds() { public Metrics getMetrics() { return metrics; } + + @Override + public boolean closeClientOnStop() { + return closeClientOnStop; + } }; } From 9f829a6016172f2e39f2ec6be555af7200503fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 12 Jan 2022 14:07:35 +0100 Subject: [PATCH 0224/1608] feature: sonar for builds (#801) --- .github/workflows/sonar.yml | 43 +++++++++++++++++++++++++++++++++++++ pom.xml | 2 ++ 2 files changed, 45 insertions(+) create mode 100644 .github/workflows/sonar.yml diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 0000000000..d5e461e70e --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,43 @@ +name: Sonar + +env: + MAVEN_ARGS: -V -ntp -e + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true +on: + push: + branches: [ main ] + pull_request: + types: [ opened, synchronize, reopened ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Java and Maven + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: 17 + cache: 'maven' + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn -B org.jacoco:jacoco-maven-plugin:prepare-agent verify org.jacoco:jacoco-maven-plugin:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=java-operator-sdk_java-operator-sdk + diff --git a/pom.xml b/pom.xml index f8227a4f65..bf2ed2d50d 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,8 @@ 11 ${java.version} ${java.version} + java-operator-sdk + https://sonarcloud.io 5.8.2 5.11.2 From 9c3f18727b328d3bed04adcdf1c7e630d9b0045d Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 12 Jan 2022 14:15:51 +0100 Subject: [PATCH 0225/1608] fix: only check for CRD if we're dealing with CustomResources (#811) Fixes #810 --- .../operator/processing/Controller.java | 9 ++- .../operator/processing/ControllerTest.java | 79 +++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java 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 e291a84803..5e93b0d7af 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 @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; +import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; @@ -167,10 +168,10 @@ public void start() throws OperatorException { try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config - if (configurationService().checkCRDAndValidateLocalModel()) { - crd = - kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName) - .get(); + if (configurationService().checkCRDAndValidateLocalModel() + && CustomResource.class.isAssignableFrom(resClass)) { + crd = kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName) + .get(); if (crd == null) { throwMissingCRDException(crdName, specVersion, controllerName); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java new file mode 100644 index 0000000000..d61b50d583 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -0,0 +1,79 @@ +package io.javaoperatorsdk.operator.processing; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.V1ApiextensionAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.ApiextensionsAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.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; +import static org.mockito.Mockito.when; + +class ControllerTest { + + @Test + void crdShouldNotBeCheckedForNativeResources() { + final var client = mock(KubernetesClient.class); + final var configurationService = mock(ConfigurationService.class); + final var reconciler = mock(Reconciler.class); + final var configuration = mock(ControllerConfiguration.class); + when(configuration.getResourceClass()).thenReturn(Secret.class); + when(configuration.getConfigurationService()).thenReturn(configurationService); + + final var controller = new Controller(reconciler, configuration, client); + controller.start(); + verify(client, never()).apiextensions(); + } + + @Test + void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { + final var client = mock(KubernetesClient.class); + final var configurationService = mock(ConfigurationService.class); + when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); + final var reconciler = mock(Reconciler.class); + final var configuration = mock(ControllerConfiguration.class); + when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); + when(configuration.getConfigurationService()).thenReturn(configurationService); + + final var controller = new Controller(reconciler, configuration, client); + controller.start(); + verify(client, never()).apiextensions(); + } + + @Test + void crdShouldBeCheckedForCustomResourcesByDefault() { + final var client = mock(KubernetesClient.class); + final var configurationService = mock(ConfigurationService.class); + when(configurationService.checkCRDAndValidateLocalModel()).thenCallRealMethod(); + final var reconciler = mock(Reconciler.class); + final var configuration = mock(ControllerConfiguration.class); + when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); + when(configuration.getConfigurationService()).thenReturn(configurationService); + final var apiGroupDSL = mock(ApiextensionsAPIGroupDSL.class); + when(client.apiextensions()).thenReturn(apiGroupDSL); + final var v1 = mock(V1ApiextensionAPIGroupDSL.class); + when(apiGroupDSL.v1()).thenReturn(v1); + final var operation = mock(NonNamespaceOperation.class); + when(v1.customResourceDefinitions()).thenReturn(operation); + when(operation.withName(any())).thenReturn(mock(Resource.class)); + + final var controller = new Controller(reconciler, configuration, client); + // since we're not really connected to a cluster and the CRD wouldn't be deployed anyway, we + // expect a MissingCRDException to be thrown + assertThrows(MissingCRDException.class, controller::start); + verify(client, times(1)).apiextensions(); + } +} From fdefd56ceb6d893dcfa0f175173a673ef44a2051 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 12 Jan 2022 16:34:26 +0100 Subject: [PATCH 0226/1608] fix: generate proper resource name & finalizer for group-less resources (#814) Fixes #812 --- .../operator/ReconcilerUtils.java | 47 ++++++++++++++++++- .../api/config/ControllerConfiguration.java | 4 +- .../operator/ReconcilerUtilsTest.java | 24 ++++++++-- .../runtime/AnnotationConfiguration.java | 13 +++-- .../DefaultConfigurationServiceTest.java | 10 ++-- .../EventSourceTestCustomReconciler.java | 4 +- .../retry/RetryTestCustomReconciler.java | 4 +- .../sample/simple/TestReconciler.java | 3 +- .../SubResourceTestCustomReconciler.java | 4 +- 9 files changed, 87 insertions(+), 26 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index dbf3d1a59e..bc0d3e921e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -2,6 +2,8 @@ import java.util.Locale; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -10,9 +12,50 @@ public class ReconcilerUtils { private static final String FINALIZER_NAME_SUFFIX = "/finalizer"; + protected static final String MISSING_GROUP_SUFFIX = ".javaoperatorsdk.io"; - public static String getDefaultFinalizerName(String crdName) { - return crdName + FINALIZER_NAME_SUFFIX; + // prevent instantiation of util class + private ReconcilerUtils() {} + + public static boolean isFinalizerValid(String finalizer) { + // todo: use fabric8 method when 5.12 is released + // return HasMetadata.validateFinalizer(finalizer); + final var validator = new HasMetadata() { + + @Override + public ObjectMeta getMetadata() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMetadata(ObjectMeta objectMeta) { + throw new UnsupportedOperationException(); + } + + @Override + public void setApiVersion(String s) { + throw new UnsupportedOperationException(); + } + }; + return validator.isFinalizerValid(finalizer); + } + + public static String getResourceTypeName(Class resourceClass) { + // todo: use fabric8 method when 5.12 is released + // return HasMetadata.getFullResourceName(resourceClass); + final var group = HasMetadata.getGroup(resourceClass); + final var plural = HasMetadata.getPlural(resourceClass); + return (group == null || group.isEmpty()) ? plural : plural + "." + group; + } + + public static String getDefaultFinalizerName(Class resourceClass) { + var resourceName = getResourceTypeName(resourceClass); + // resource names for historic resources such as Pods are missing periods and therefore do not + // constitute valid domain names as mandated by Kubernetes so generate one that does + if (resourceName.indexOf('.') < 0) { + resourceName = resourceName + MISSING_GROUP_SUFFIX; + } + return resourceName + FINALIZER_NAME_SUFFIX; } public static String getNameFor(Class reconcilerClass) { 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 1353e6ea38..b85df49eba 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,11 +17,11 @@ default String getName() { } default String getResourceTypeName() { - return HasMetadata.getFullResourceName(getResourceClass()); + return ReconcilerUtils.getResourceTypeName(getResourceClass()); } default String getFinalizer() { - return ReconcilerUtils.getDefaultFinalizerName(getResourceTypeName()); + return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } /** diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index 47870e4f93..cf97be1d2d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -2,17 +2,35 @@ import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.Pod; import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultFinalizerName; +import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultNameFor; +import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultReconcilerName; +import static io.javaoperatorsdk.operator.ReconcilerUtils.isFinalizerValid; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class ReconcilerUtilsTest { @Test - void getDefaultResourceControllerName() { + void defaultReconcilerNameShouldWork() { assertEquals( "testcustomreconciler", - ReconcilerUtils.getDefaultReconcilerName( - TestCustomReconciler.class.getCanonicalName())); + getDefaultReconcilerName(TestCustomReconciler.class.getCanonicalName())); + assertEquals( + getDefaultNameFor(TestCustomReconciler.class), + getDefaultReconcilerName(TestCustomReconciler.class.getCanonicalName())); + assertEquals( + getDefaultNameFor(TestCustomReconciler.class), + getDefaultReconcilerName(TestCustomReconciler.class.getSimpleName())); + } + + @Test + void defaultFinalizerShouldWork() { + assertTrue(isFinalizerValid(getDefaultFinalizerName(Pod.class))); + assertTrue(isFinalizerValid(getDefaultFinalizerName(TestCustomResource.class))); } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 7bda61874c..a02db55e22 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +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.processing.event.source.controller.ResourceEventFilter; @@ -31,9 +32,15 @@ public String getName() { @Override public String getFinalizer() { if (annotation == null || annotation.finalizerName().isBlank()) { - return ReconcilerUtils.getDefaultFinalizerName(getResourceTypeName()); + return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } else { - return annotation.finalizerName(); + final var finalizer = annotation.finalizerName(); + if (Constants.NO_FINALIZER.equals(finalizer) || 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"); + } } } @@ -93,7 +100,7 @@ public ResourceEventFilter getEventFilter() { answer = answer.and(filter); } } catch (Exception e) { - throw new RuntimeException(e); + throw new IllegalArgumentException(e); } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 9c4bf8c9ab..bce957a399 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -22,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -public class DefaultConfigurationServiceTest { +class DefaultConfigurationServiceTest { public static final String CUSTOM_FINALIZER_NAME = "a.custom/finalizer"; @@ -73,21 +73,21 @@ void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { } @Test - public void returnsValuesFromControllerAnnotationFinalizer() { + void returnsValuesFromControllerAnnotationFinalizer() { final var reconciler = new TestCustomReconciler(); final var configuration = DefaultConfigurationService.instance().getConfigurationFor(reconciler); assertEquals(CustomResource.getCRDName(TestCustomResource.class), configuration.getResourceTypeName()); assertEquals( - ReconcilerUtils.getDefaultFinalizerName(configuration.getResourceTypeName()), + ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class), configuration.getFinalizer()); assertEquals(TestCustomResource.class, configuration.getResourceClass()); assertFalse(configuration.isGenerationAware()); } @Test - public void returnCustomerFinalizerNameIfSet() { + void returnCustomerFinalizerNameIfSet() { final var reconciler = new TestCustomFinalizerReconciler(); final var configuration = DefaultConfigurationService.instance().getConfigurationFor(reconciler); @@ -95,7 +95,7 @@ public void returnCustomerFinalizerNameIfSet() { } @Test - public void supportsInnerClassCustomResources() { + void supportsInnerClassCustomResources() { final var reconciler = new TestCustomFinalizerReconciler(); assertDoesNotThrow( () -> { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java index 3319ecc86c..55eb61634f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java @@ -2,7 +2,6 @@ import java.util.concurrent.atomic.AtomicInteger; -import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -14,8 +13,7 @@ public class EventSourceTestCustomReconciler TestExecutionInfoProvider { public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName( - CustomResource.getCRDName(EventSourceTestCustomResource.class)); + ReconcilerUtils.getDefaultFinalizerName(EventSourceTestCustomResource.class); public static final int TIMER_PERIOD = 500; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index df26745def..a76b5b0113 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -5,7 +5,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -18,8 +17,7 @@ public class RetryTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName( - CustomResource.getCRDName(RetryTestCustomResource.class)); + ReconcilerUtils.getDefaultFinalizerName(RetryTestCustomResource.class); private static final Logger log = LoggerFactory.getLogger(RetryTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index bd32ad2bd3..94a7d36868 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -10,7 +10,6 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; @@ -26,7 +25,7 @@ public class TestReconciler private static final Logger log = LoggerFactory.getLogger(TestReconciler.class); public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName(CustomResource.getCRDName(TestCustomResource.class)); + ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private KubernetesClient kubernetesClient; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index 431374392f..081100beb3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -5,7 +5,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -18,8 +17,7 @@ public class SubResourceTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName( - CustomResource.getCRDName(SubResourceTestCustomResource.class)); + ReconcilerUtils.getDefaultFinalizerName(SubResourceTestCustomResource.class); private static final Logger log = LoggerFactory.getLogger(SubResourceTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); From f6d5e6bf0e4d7de633df4be619d82654173bcf76 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 12 Jan 2022 19:56:33 +0100 Subject: [PATCH 0227/1608] feat: remove server version output (#817) --- .../io/javaoperatorsdk/operator/Operator.java | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) 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 f8da4ee8a8..d0c9b59f2c 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 @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator; -import java.net.ConnectException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -76,22 +75,8 @@ public void start() { version.getCommit(), version.getBuiltTime()); - log.info("Client version: {}", Version.clientVersion()); - try { - final var k8sVersion = kubernetesClient.getKubernetesVersion(); - if (k8sVersion != null) { - log.info("Server version: {}.{}", k8sVersion.getMajor(), k8sVersion.getMinor()); - } - } catch (Exception e) { - final String error; - if (e.getCause() instanceof ConnectException) { - error = "Cannot connect to cluster"; - } else { - error = "Error retrieving the server version"; - } - log.error(error, e); - throw new OperatorException(error, e); - } + final var clientVersion = Version.clientVersion(); + log.info("Client version: {}", clientVersion); ExecutorServiceManager.init(configurationService); controllers.start(); From 84956ab2296f380968ee75b6fab14974251b5f39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 08:50:34 +0100 Subject: [PATCH 0228/1608] chore(deps): bump maven-jar-plugin from 3.2.1 to 3.2.2 (#821) Bumps [maven-jar-plugin](https://github.com/apache/maven-jar-plugin) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/apache/maven-jar-plugin/releases) - [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.2.1...maven-jar-plugin-3.2.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-jar-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bf2ed2d50d..a9e9d22721 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ 3.3.1 3.2.0 3.2.1 - 3.2.1 + 3.2.2 3.1.0 3.0.1 1.6.8 From c4b635b64f86a5243fdbccb80b3d1e0f922df581 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 08:50:44 +0100 Subject: [PATCH 0229/1608] chore(deps): bump maven-compiler-plugin from 3.8.1 to 3.9.0 (#822) Bumps [maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.8.1 to 3.9.0. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.8.1...maven-compiler-plugin-3.9.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index a9e9d22721..edee794953 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 1.8.1 2.11 - 3.8.1 + 3.9.0 3.0.0-M5 3.3.1 3.2.0 diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index c07edc2583..1b64e52a1f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -94,7 +94,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.9.0 diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index cfcfdfb19a..48e3d2fb50 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -84,7 +84,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.9.0 diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 34286abadc..0105bbc3e6 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -60,7 +60,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.9.0 From 52fc813430399cabe3cc8c42015b1af98edecddf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 08:53:08 +0100 Subject: [PATCH 0230/1608] chore(deps): bump actions/cache from 1 to 2.1.7 (#820) Bumps [actions/cache](https://github.com/actions/cache) from 1 to 2.1.7. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v1...v2.1.7) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sonar.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index d5e461e70e..0af865458b 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -24,13 +24,13 @@ jobs: java-version: 17 cache: 'maven' - name: Cache SonarCloud packages - uses: actions/cache@v1 + uses: actions/cache@v2.1.7 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Maven packages - uses: actions/cache@v1 + uses: actions/cache@v2.1.7 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} From bd242ea6f37c8517b7da7dd947d309a107cdd8a1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 13 Jan 2022 09:28:31 +0100 Subject: [PATCH 0231/1608] fix: ReconcilerUtils.isFinalizerValid should account for NO_FINALIZER (#819) Also restores ReconcilerUtils.getDefaultReconcilerName(String) Fixes #818 --- .../java/io/javaoperatorsdk/operator/ReconcilerUtils.java | 7 +++++-- .../io/javaoperatorsdk/operator/ReconcilerUtilsTest.java | 6 ++++++ .../operator/config/runtime/AnnotationConfiguration.java | 3 +-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index bc0d3e921e..26dc4cdcca 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -37,7 +37,7 @@ public void setApiVersion(String s) { throw new UnsupportedOperationException(); } }; - return validator.isFinalizerValid(finalizer); + return Constants.NO_FINALIZER.equals(finalizer) || validator.isFinalizerValid(finalizer); } public static String getResourceTypeName(Class resourceClass) { @@ -49,7 +49,10 @@ public static String getResourceTypeName(Class resourceCl } public static String getDefaultFinalizerName(Class resourceClass) { - var resourceName = getResourceTypeName(resourceClass); + return getDefaultFinalizerName(getResourceTypeName(resourceClass)); + } + + public static String getDefaultFinalizerName(String resourceName) { // resource names for historic resources such as Pods are missing periods and therefore do not // constitute valid domain names as mandated by Kubernetes so generate one that does if (resourceName.indexOf('.') < 0) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index cf97be1d2d..d46936b3d5 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.Pod; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -33,4 +34,9 @@ void defaultFinalizerShouldWork() { assertTrue(isFinalizerValid(getDefaultFinalizerName(Pod.class))); assertTrue(isFinalizerValid(getDefaultFinalizerName(TestCustomResource.class))); } + + @Test + void noFinalizerMarkerShouldWork() { + assertTrue(isFinalizerValid(Constants.NO_FINALIZER)); + } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index a02db55e22..1000cb61c4 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -6,7 +6,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -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.processing.event.source.controller.ResourceEventFilter; @@ -35,7 +34,7 @@ public String getFinalizer() { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } else { final var finalizer = annotation.finalizerName(); - if (Constants.NO_FINALIZER.equals(finalizer) || ReconcilerUtils.isFinalizerValid(finalizer)) { + if (ReconcilerUtils.isFinalizerValid(finalizer)) { return finalizer; } else { throw new IllegalArgumentException(finalizer From d42942b034856bdd510b21e99d67ef58c7dbd428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 13 Jan 2022 11:04:47 +0100 Subject: [PATCH 0232/1608] Sonar reported issues (#816) --- .../io/javaoperatorsdk/operator/Operator.java | 12 +++-------- .../retry/GenericRetryExecution.java | 10 ++++++---- .../event/ReconciliationDispatcherTest.java | 2 +- ....java => AbstractEventSourceTestBase.java} | 2 +- .../event/source/CachingEventSourceTest.java | 2 +- .../source/CustomResourceSelectorTest.java | 10 +++++++--- .../ControllerResourceEventSourceTest.java | 4 ++-- .../PerResourcePollingEventSourceTest.java | 4 ++-- .../polling/PollingEventSourceTest.java | 4 ++-- .../source/timer/TimerEventSourceTest.java | 5 +++-- .../operator/junit/OperatorExtension.java | 2 +- .../runtime/AccumulativeMappingWriter.java | 20 +++++++++---------- .../config/runtime/ClassMappingProvider.java | 7 ++++--- ...ollerConfigurationAnnotationProcessor.java | 10 ++++++++-- 14 files changed, 51 insertions(+), 43 deletions(-) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/{AbstractEventSourceTest.java => AbstractEventSourceTestBase.java} (90%) 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 d0c9b59f2c..8ece5334fd 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 @@ -20,7 +20,7 @@ import io.javaoperatorsdk.operator.processing.LifecycleAware; @SuppressWarnings("rawtypes") -public class Operator implements AutoCloseable, LifecycleAware { +public class Operator implements LifecycleAware { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient kubernetesClient; private final ConfigurationService configurationService; @@ -43,9 +43,9 @@ public Operator(KubernetesClient kubernetesClient, ConfigurationService configur this.configurationService = configurationService; } - /** Adds a shutdown hook that automatically calls {@link #close()} when the app shuts down. */ + /** Adds a shutdown hook that automatically calls {@link #stop()} ()} when the app shuts down. */ public void installShutdownHook() { - Runtime.getRuntime().addShutdownHook(new Thread(this::close)); + Runtime.getRuntime().addShutdownHook(new Thread(this::stop)); } public KubernetesClient getKubernetesClient() { @@ -95,12 +95,6 @@ public void stop() throws OperatorException { } } - /** Stop the operator. */ - @Override - public void close() { - stop(); - } - /** * Add a registration requests for the specified reconciler with this operator. The effective * registration of the reconciler is delayed till the operator is started. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecution.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecution.java index db06363ced..32bf154f97 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecution.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetryExecution.java @@ -6,8 +6,8 @@ public class GenericRetryExecution implements RetryExecution { private final GenericRetry genericRetry; - private volatile int lastAttemptIndex = 0; - private volatile long currentInterval; + private int lastAttemptIndex = 0; + private long currentInterval; public GenericRetryExecution(GenericRetry genericRetry) { this.genericRetry = genericRetry; @@ -15,7 +15,8 @@ public GenericRetryExecution(GenericRetry genericRetry) { } public Optional nextDelay() { - if (genericRetry.getMaxAttempts() > -1 && lastAttemptIndex >= genericRetry.getMaxAttempts()) { + if (genericRetry.getMaxAttempts() > -1 + && lastAttemptIndex >= genericRetry.getMaxAttempts()) { return Optional.empty(); } if (lastAttemptIndex > 1) { @@ -30,7 +31,8 @@ public Optional nextDelay() { @Override public boolean isLastAttempt() { - return genericRetry.getMaxAttempts() > -1 && lastAttemptIndex >= genericRetry.getMaxAttempts(); + return genericRetry.getMaxAttempts() > -1 + && lastAttemptIndex >= genericRetry.getMaxAttempts(); } @Override 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 5e642a94b5..1c43faf5f6 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 @@ -103,7 +103,7 @@ void addFinalizerOnNewResource() { verify(customResourceFacade, times(1)) .replaceWithLock( argThat(testCustomResource -> testCustomResource.hasFinalizer(DEFAULT_FINALIZER))); - assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); + assertThat(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)).isTrue(); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTestBase.java similarity index 90% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTestBase.java index 68479225f7..6aa974e695 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSourceTestBase.java @@ -6,7 +6,7 @@ import static org.mockito.Mockito.mock; -public class AbstractEventSourceTest { +public class AbstractEventSourceTestBase { protected T eventHandler; protected S source; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java index f0d81f47ba..29cf0a981d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java @@ -12,7 +12,7 @@ import static org.mockito.Mockito.*; class CachingEventSourceTest extends - AbstractEventSourceTest, EventHandler> { + AbstractEventSourceTestBase, EventHandler> { @BeforeEach public void setup() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index 22be8649e6..ce865b7888 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -60,9 +60,9 @@ void resourceWatchedByLabel() { assertThat(server).isNotNull(); assertThat(client).isNotNull(); - try (Operator o1 = new Operator(client, configurationService); - Operator o2 = new Operator(client, configurationService)) { - + Operator o1 = new Operator(client, configurationService); + Operator o2 = new Operator(client, configurationService); + try { AtomicInteger c1 = new AtomicInteger(); AtomicInteger c1err = new AtomicInteger(); AtomicInteger c2 = new AtomicInteger(); @@ -113,7 +113,11 @@ void resourceWatchedByLabel() { assertThrows( ConditionTimeoutException.class, () -> await().atMost(2, TimeUnit.SECONDS).untilAtomic(c2err, is(greaterThan(0)))); + } finally { + o1.stop(); + o2.stop(); } + } public TestCustomResource newMyResource(String app, String namespace) { 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 70b208a194..5cdc85c553 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 @@ -15,7 +15,7 @@ import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTest; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.ArgumentMatchers.eq; @@ -25,7 +25,7 @@ import static org.mockito.Mockito.verify; class ControllerResourceEventSourceTest extends - AbstractEventSourceTest, EventHandler> { + AbstractEventSourceTestBase, EventHandler> { public static final String FINALIZER = "finalizer"; private static final MixedOperation, Resource> client = diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java index eea42f8f69..8486f705ca 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java @@ -8,7 +8,7 @@ import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTest; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -24,7 +24,7 @@ import static org.mockito.Mockito.when; class PerResourcePollingEventSourceTest extends - AbstractEventSourceTest, EventHandler> { + AbstractEventSourceTestBase, EventHandler> { public static final int PERIOD = 80; private PerResourcePollingEventSource.ResourceSupplier supplier = 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 40fcba1371..db1ea74aab 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 @@ -10,7 +10,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTest; +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.*; @@ -18,7 +18,7 @@ class PollingEventSourceTest extends - AbstractEventSourceTest, EventHandler> { + AbstractEventSourceTestBase, EventHandler> { private Supplier> supplier = mock(Supplier.class); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java index 3ae4f1bb92..2dc42c0b16 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/timer/TimerEventSourceTest.java @@ -15,7 +15,7 @@ import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTest; +import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSourceTest.CapturingEventHandler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -23,7 +23,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class TimerEventSourceTest - extends AbstractEventSourceTest, CapturingEventHandler> { + extends + AbstractEventSourceTestBase, CapturingEventHandler> { public static final int INITIAL_DELAY = 50; public static final int PERIOD = 50; diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index fecf2acd15..797e0db6f7 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -206,7 +206,7 @@ protected void after(ExtensionContext context) { } try { - this.operator.close(); + this.operator.stop(); } catch (Exception e) { // ignored } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java index d0302e2099..6f37572e24 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AccumulativeMappingWriter.java @@ -35,14 +35,15 @@ public AccumulativeMappingWriter loadExistingMappings() { .getFiler() .getResource(StandardLocation.CLASS_OUTPUT, "", resourcePath); - final var bufferedReader = - new BufferedReader(new InputStreamReader(readonlyResource.openInputStream())); - final var existingLines = - bufferedReader - .lines() - .map(l -> l.split(",")) - .collect(Collectors.toMap(parts -> parts[0], parts -> parts[1])); - mappings.putAll(existingLines); + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(readonlyResource.openInputStream()))) { + final var existingLines = + bufferedReader + .lines() + .map(l -> l.split(",")) + .collect(Collectors.toMap(parts -> parts[0], parts -> parts[1])); + mappings.putAll(existingLines); + } } catch (IOException e) { } return this; @@ -71,8 +72,7 @@ public void flush() { printWriter.println(entry.getKey() + "," + entry.getValue()); } } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException(e); + throw new IllegalStateException(e); } finally { if (printWriter != null) { printWriter.close(); diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java index f2f0167de0..d3559a9201 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ClassMappingProvider.java @@ -55,8 +55,9 @@ static Map provide(final String resourcePath, T key, V value) { } private static List retrieveClassNamePairs(URL url) throws IOException { - return new BufferedReader(new InputStreamReader(url.openStream())) - .lines() - .collect(Collectors.toList()); + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines() + .collect(Collectors.toList()); + } } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java index df509f942e..d1cdb28581 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/ControllerConfigurationAnnotationProcessor.java @@ -15,6 +15,9 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -28,6 +31,9 @@ @AutoService(Processor.class) public class ControllerConfigurationAnnotationProcessor extends AbstractProcessor { + private static final Logger log = + LoggerFactory.getLogger(ControllerConfigurationAnnotationProcessor.class); + private AccumulativeMappingWriter controllersResourceWriter; private TypeParameterResolver typeParameterResolver; @@ -89,7 +95,7 @@ private void recordCRType(TypeElement controllerClassSymbol) { controllerClassSymbol.getQualifiedName().toString(), customResourceType.toString()); } catch (Exception ioException) { - ioException.printStackTrace(); + log.error("Error", ioException); } } @@ -99,7 +105,7 @@ private TypeMirror findResourceType(TypeElement controllerClassSymbol) { return typeParameterResolver.resolve( processingEnv.getTypeUtils(), (DeclaredType) controllerClassSymbol.asType()); } catch (Exception e) { - e.printStackTrace(); + log.error("Error", e); return null; } } From 001cbaf38bc91332f0bced22649e3e7a465857ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 13 Jan 2022 13:14:36 +0100 Subject: [PATCH 0233/1608] fix: remove unnecessary cache (#825) --- .github/workflows/sonar.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 0af865458b..df359ea008 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -29,12 +29,6 @@ jobs: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v2.1.7 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any From a22596ef79b87e6b8eeea2e8fef4b87ea3fa367f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Jan 2022 08:56:55 +0100 Subject: [PATCH 0234/1608] chore(deps): bump slf4j-api from 1.7.32 to 1.7.33 (#832) Bumps [slf4j-api](https://github.com/qos-ch/slf4j) from 1.7.32 to 1.7.33. - [Release notes](https://github.com/qos-ch/slf4j/releases) - [Commits](https://github.com/qos-ch/slf4j/compare/v_1.7.32...v_1.7.33) --- updated-dependencies: - dependency-name: org.slf4j:slf4j-api dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index edee794953..0374ad917d 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 5.8.2 5.11.2 - 1.7.32 + 1.7.33 2.17.1 4.2.0 3.12.0 From 918463324f424b493540b124dee05c777594a4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 14 Jan 2022 11:15:12 +0100 Subject: [PATCH 0235/1608] fix: potential bug in informers (#833) --- .../source/informer/InformerEventSource.java | 7 ++++++ .../operator/junit/OperatorExtension.java | 3 +++ .../operator/InformerEventSourceIT.java | 25 ++++++++++++++++--- ...formerEventSourceTestCustomReconciler.java | 24 +++++++++++++----- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index dadf24aa74..eada096713 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -83,6 +83,13 @@ public void onAdd(T t) { @Override public void onUpdate(T oldObject, T newObject) { + if (newObject == null) { + // this is a fix for this potential issue with informer: + // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 + propagateEvent(oldObject); + return; + } + if (InformerEventSource.this.skipUpdateEventPropagationIfNoChange && oldObject.getMetadata().getResourceVersion() .equals(newObject.getMetadata().getResourceVersion())) { diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 797e0db6f7..03208422de 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -140,6 +140,9 @@ public T replace(Class type, T resource) { return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); } + public boolean delete(Class type, T resource) { + return kubernetesClient.resources(type).inNamespace(namespace).delete(resource); + } @SuppressWarnings("unchecked") protected void before(ExtensionContext context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 06a1238bb5..4885e7d356 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -13,9 +13,9 @@ import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResource; -import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.RELATED_RESOURCE_NAME; -import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.TARGET_CONFIG_MAP_KEY; +import static io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; public class InformerEventSourceIT { @@ -32,7 +32,7 @@ public class InformerEventSourceIT { .build(); @Test - public void testUsingInformerToWatchChangesOfConfigMap() { + void testUsingInformerToWatchChangesOfConfigMap() { var customResource = initialCustomResource(); customResource = operator.create(InformerEventSourceTestCustomResource.class, customResource); ConfigMap configMap = @@ -45,6 +45,25 @@ public void testUsingInformerToWatchChangesOfConfigMap() { waitForCRStatusValue(UPDATE_STATUS_MESSAGE); } + @Test + void deletingSecondaryResource() { + var customResource = initialCustomResource(); + customResource = operator.create(InformerEventSourceTestCustomResource.class, customResource); + ConfigMap configMap = + operator.create(ConfigMap.class, relatedConfigMap(customResource.getMetadata().getName())); + waitForCRStatusValue(INITIAL_STATUS_MESSAGE); + + boolean res = operator.delete(ConfigMap.class, configMap); + if (!res) { + fail("Unable to delete configmap"); + } + + waitForCRStatusValue(MISSING_CONFIG_MAP); + assertThat(((InformerEventSourceTestCustomReconciler) operator.getReconcilers().get(0)) + .getNumberOfExecutions()) + .isEqualTo(3); + } + private ConfigMap relatedConfigMap(String relatedResourceAnnotation) { ConfigMap configMap = new ConfigMap(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 28807339df..b7ceb4c11c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.sample.informereventsource; import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,8 +36,10 @@ public class InformerEventSourceTestCustomReconciler implements public static final String RELATED_RESOURCE_NAME = "relatedResourceName"; public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; + public static final String MISSING_CONFIG_MAP = "Missing Config Map"; private KubernetesClient kubernetesClient; + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @Override public List prepareEventSources( @@ -48,16 +52,20 @@ public List prepareEventSources( public UpdateControl reconcile( InformerEventSourceTestCustomResource resource, Context context) { + numberOfExecutions.incrementAndGet(); + resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); // Reading the config map from the informer not from the API // name of the config map same as custom resource for sake of simplicity - ConfigMap configMap = context.getSecondaryResource(ConfigMap.class) - .orElseThrow(() -> new IllegalStateException("Config map should be present.")); + Optional configMap = context.getSecondaryResource(ConfigMap.class); + if (configMap.isEmpty()) { + resource.getStatus().setConfigMapValue(MISSING_CONFIG_MAP); + } else { + String targetStatus = configMap.get().getData().get(TARGET_CONFIG_MAP_KEY); + LOGGER.debug("Setting target status for CR: {}", targetStatus); + resource.getStatus().setConfigMapValue(targetStatus); + } - String targetStatus = configMap.getData().get(TARGET_CONFIG_MAP_KEY); - LOGGER.debug("Setting target status for CR: {}", targetStatus); - resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); - resource.getStatus().setConfigMapValue(targetStatus); return UpdateControl.updateStatus(resource); } @@ -70,4 +78,8 @@ public KubernetesClient getKubernetesClient() { public void setKubernetesClient(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } } From bed8ebc195d9738fefe8a9cb3b1cc88382e8bbb7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 14 Jan 2022 14:26:06 +0000 Subject: [PATCH 0236/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 2866f42257..fc055ea220 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 47fc3f9881..54409af136 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 7a9977ded0..0339129c75 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 2fe67f8716..0c05632099 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 0374ad917d..db0d6dc66c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 1b64e52a1f..6f021b383f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index cb47a55049..d186916abd 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 48e3d2fb50..b952d03f86 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 0105bbc3e6..9dde813564 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index ed1884aecd..0038730ea3 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 566fbd98be..2d1ad2923a 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index d1a2de4c2b..9fb7f1c92c 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index ce82ac6b2d..27e5c01d70 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 19362a90c275dcafb4bb4395d7327064f1e21633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 14 Jan 2022 17:09:43 +0100 Subject: [PATCH 0237/1608] docs: /readme.md and docs update (#834) --- README.md | 312 ++------------------------ docs/documentation/faq.md | 7 + docs/documentation/features.md | 25 +++ docs/documentation/getting-started.md | 3 +- 4 files changed, 55 insertions(+), 292 deletions(-) diff --git a/README.md b/README.md index 4ea7d5ade3..f6f31e357f 100644 --- a/README.md +++ b/README.md @@ -3,312 +3,42 @@ ![Java CI with Maven](https://github.com/java-operator-sdk/java-operator-sdk/workflows/Java%20CI%20with%20Maven/badge.svg) [![Discord](https://img.shields.io/discord/723455000604573736.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.com/channels/723455000604573736) -Build Kubernetes Operators in Java without hassle. Inspired -by [operator-sdk](https://github.com/operator-framework/operator-sdk). +#Build Kubernetes Operators in Java Without Hassle -Our webpage with documentation is getting better every day: https://javaoperatorsdk.io/ +## Documentation -**!! NOTE the main branch now contains source code for v2, which is under development, for actual -version see [v1 branch](https://github.com/java-operator-sdk/java-operator-sdk/tree/v1) !!** +Documentation can be found on the **[JOSDK WebSite](https://javaoperatorsdk.io/)**. -Table of Contents -========== +It's getting better every day! :) -1. [Features](#Features) -1. [Why build your own Operator?](#Why-build-your-own-Operator) -1. [Roadmap and Release Notes](#Roadmap-and-Release-Notes) -1. [Join us on Discord!](#Join-us-on-Discord) -1. [Usage](#Usage) +## Contact us -## Features +Join us on [Discord](https://discord.gg/DacEhAy) or feel free to ask any question on +[Kubernetes Slack Operator Channel](https://kubernetes.slack.com/archives/CAW0GV7A5) + +## How to Contribute + +See the [contribution](https://javaoperatorsdk.io/docs/contributing) guide on the website. + +## What is Java Operator SDK + +Java Operator SDK is a higher level framework and related tooling to support writing Kubernetes Operators in Java. +It makes it use to utilize best practices and patters of an Operator development. Features include: * Framework for handling Kubernetes API events * Automatic registration of Custom Resource watches * Retry action on failure * Smart event scheduling (only handle the latest event for the same resource) * Handling Observed Generations automatically -* for all see [features](https://javaoperatorsdk.io/docs/features) section on the webpage - -Check out -this [blog post](https://csviri.medium.com/deep-dive-building-a-kubernetes-operator-sdk-for-java-developers-5008218822cb) -about the non-trivial yet common problems needed to be solved for every operator. In case you are interested how to -handle more complex scenarios take a look -on [event sources](https://csviri.medium.com/java-operator-sdk-introduction-to-event-sources-a1aab5af4b7b) -. - -## Why build your own Operator? - -* Infrastructure automation using the power and flexibility of Java. - See [blog post](https://blog.container-solutions.com/cloud-native-java-infrastructure-automation-with-kubernetes-operators) - . -* Provisioning of complex applications - avoiding Helm chart hell -* Integration with Cloud services - e.g. Secret stores -* Safer deployment of applications - only expose cluster to users by Custom Resources - -#### Overview of the 1.9.0 changes - -- The Spring Boot starters have been moved to their own repositories and are now found at: - - https://github.com/java-operator-sdk/operator-framework-spring-boot-starter - - https://github.com/java-operator-sdk/operator-framework-spring-boot-starter-test -- Updated Fabric8 client to version 5.4.0 -- It is now possible to configure the controllers to not automatically add finalizers to resources. See the `Controller` - annotation documentation for more details. -- Added the possibility to configure how many seconds the SDK will wait before terminating reconciliation threads when a - shut down is requested - -#### Overview of the 1.8.0 changes - -- The quarkus extension has been moved to the quarkiverse and is now found at - https://github.com/quarkiverse/quarkus-operator-sdk - -##### Overview of the 1.7.0 changes - -- `Doneable` classes have been removed along with all the involved complexity -- `Controller` annotation has been simplified: the `crdName` field has been removed as that value is computed from the - associated custom resource implementation -- Custom Resource implementation classes now need to be annotated with `Group` and `Version` - annotations so that they can be identified properly. Optionally, they can also be annotated with - `Kind` (if the name of the implementation class doesn't match the desired kind) and `Plural` if the plural version - cannot be automatically computed (or the default computed version doesn't match your expectations). -- The `CustomResource` class that needs to be extended is now parameterized with spec and status types, so you can have - an empty default implementation that does what you'd expect. If you don't need a status, using `Void` for the - associated type should work. -- Custom Resources that are namespace-scoped need to implement the `Namespaced` interface so that the client can - generate the proper URLs. This means, in particular, that `CustomResource` - implementations that do **not** implement `Namespaced` are considered cluster-scoped. As a consequence, - the `isClusterScoped` method/field has been removed from the appropriate classes (`Controller` annotation, in - particular) as this is now inferred from the `CustomResource` - type associated with your `Controller`. - -Many of these changes might not be immediately apparent but will result in `404` errors when connecting to the cluster. -Please check that the Custom Resource implementations are properly annotated and that the value corresponds to your CRD -manifest. If the namespace appear to be missing in your request URL, don't forget that namespace-scoped Custom Resources -need to implement the `Namescaped` interface. - -## Join us on Discord! - -[Discord Invite Link](https://discord.gg/DacEhAy) - -## Usage - -We have several simple Operators under the [smoke-test-samples](smoke-test-samples) directory: - -* *pure-java*: Minimal Operator implementation which only parses the Custom Resource and prints to stdout. Implemented - with and without Spring Boot support. The two samples share the common module. -* *spring-boot-plain*: Sample showing integration with Spring Boot. - -There are also more complete samples in the standalone [sample-operators](sample-operators): - -* *webserver*: Simple example creating an NGINX webserver from a Custom Resource containing HTML code. -* *mysql-schema*: Operator managing schemas in a MySQL database. -* *tomcat*: Operator with two controllers, managing Tomcat instances and Webapps for these. - -Add [dependency](https://search.maven.org/search?q=a:operator-framework) to your project with Maven: - -```xml - - - io.javaoperatorsdk - operator-framework - {see https://search.maven.org/search?q=a:operator-framework for latest version} - -``` - -Or alternatively with Gradle, which also requires declaring the SDK as an annotation processor to generate the mappings -between controllers and custom resource classes: - -```groovy -dependencies { - implementation "io.javaoperatorsdk:operator-framework:${javaOperatorVersion}" - annotationProcessor "io.javaoperatorsdk:operator-framework:${javaOperatorVersion}" -} -``` - -Once you've added the dependency, define a main method initializing the Operator and registering a controller. - -```java -public class Runner { - - public static void main(String[] args) { - Operator operator = new Operator(DefaultConfigurationService.instance()); - operator.register(new WebPageReconciler()); - operator.start(); - } -} -``` - -The Controller implements the business logic and describes all the classes needed to handle the CRD. - -```java - -@ControllerConfiguration -public class WebPageReconciler implements Reconciler { - - // Return the changed resource, so it gets updated. See javadoc for details. - @Override - public UpdateControl reconcile(WebPage resource, - Context context) { - // ... your logic ... - return UpdateControl.updateStatus(resource); - } -} -``` - -A sample custom resource POJO representation - -```java - -@Group("sample.javaoperatorsdk") -@Version("v1") -public class WebPage extends CustomResource implements - Namespaced { - -} - -public class WebPageSpec { - - private String html; - - public String getHtml() { - return html; - } - - public void setHtml(String html) { - this.html = html; - } -} -``` - -### Deactivating CustomResource implementations validation - -The operator will, by default, query the deployed CRDs to check that the `CustomResource` -implementations match what is known to the cluster. This requires an additional query to the cluster and, sometimes, -elevated privileges for the operator to be able to read the CRDs from the cluster. This validation is mostly meant to -help users new to operator development get started and avoid common mistakes. Advanced users or production deployments -might want to skip this step. This is done by setting the `CHECK_CRD_ENV_KEY` environment variable to `false`. - -### Automatic generation of CRDs - -To automatically generate CRD manifests from your annotated Custom Resource classes, you only need to add the following -dependencies to your project: - -```xml - - - io.fabric8 - crd-generator-apt - provided - -``` - -The CRD will be generated in `target/classes/META-INF/fabric8` (or in `target/test-classes/META-INF/fabric8`, if you use -the `test` scope) with the CRD name suffixed by the generated spec version. For example, a CR using -the `java-operator-sdk.io` group with a `mycrs` plural form will result in 2 files: - -- `mycrs.java-operator-sdk.io-v1.yml` -- `mycrs.java-operator-sdk.io-v1beta1.yml` - -**NOTE:** -> Quarkus users using the `quarkus-operator-sdk` extension do not need to add any extra dependency to get their CRD generated as this is handled by the extension itself. - -### Quarkus - -A [Quarkus](https://quarkus.io) extension is also provided to ease the development of Quarkus-based operators. - -Add [this dependency](https://search.maven.org/search?q=a:quarkus-operator-sdk) -to your project: - -```xml - - - io.quarkiverse.operatorsdk - quarkus-operator-sdk - {see https://search.maven.org/search?q=a:quarkus-operator-sdk for latest version} - - -``` - -Create an Application, Quarkus will automatically create and inject a `KubernetesClient` ( -or `OpenShiftClient`), `Operator`, `ConfigurationService` and `ResourceController` instances that your application can -use. Below, you can see the minimal code you need to write to get your operator and controllers up and running: - -```java - -@QuarkusMain -public class QuarkusOperator implements QuarkusApplication { - - @Inject - Operator operator; - - public static void main(String... args) { - Quarkus.run(QuarkusOperator.class, args); - } - - @Override - public int run(String... args) throws Exception { - operator.start(); - Quarkus.waitForExit(); - return 0; - } -} -``` - -### Spring Boot - -You can also let Spring Boot wire your application together and automatically register the controllers. - -Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter) to your project: - -```xml - - - io.javaoperatorsdk - operator-framework-spring-boot-starter - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for - latest version} - - -``` - -Create an Application - -```java - -@SpringBootApplication -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} -``` - -#### Spring Boot test support +* Support For Intelligent Error Handling -Adding the following dependency would let you mock the operator for the tests where loading the spring container is -necessary, but it doesn't need real access to a Kubernetes cluster. +For all features and their usage see the [related section on the website](https://javaoperatorsdk.io/docs/features). -```xml +## Related Projects - - io.javaoperatorsdk - operator-framework-spring-boot-starter-test - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for - latest version} - - -``` +Operator SDK plugin: https://github.com/operator-framework/java-operator-plugins -Mock the operator: +Quarkus Extension: https://github.com/quarkiverse/quarkus-operator-sdk -```java -@SpringBootTest -@EnableMockOperator -public class SpringBootStarterSampleApplicationTest { - @Test - void contextLoads() { - } -} -``` diff --git a/docs/documentation/faq.md b/docs/documentation/faq.md index f4127b39fe..fd7f2ff46b 100644 --- a/docs/documentation/faq.md +++ b/docs/documentation/faq.md @@ -5,3 +5,10 @@ layout: docs permalink: /docs/faq --- +## Q: How can I access events? +In the v1.* version events were exposed to `Reconciler` (in v1 called `ResourceController`). This +included events (Create, Update) of the custom resource, but also events produced by Event Sources. After +long discussions also with developers of golang version (controller-runtime), we decided to remove access to +these events. We already advocated to not use events in the reconciliation logic, since events can be lost. +Instead reconcile all the resources on every execution of reconciliation. On first this might sound a little +opinionated, but there was a sound agreement between the developers that this is the way to go. \ No newline at end of file diff --git a/docs/documentation/features.md b/docs/documentation/features.md index dd8be46ecf..a143316370 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -281,6 +281,31 @@ following attributes are available in most parts of reconciliation logic and dur For more information about MDC see this [link](https://www.baeldung.com/mdc-in-log4j-2-logback). +## Automatic generation of CRDs + +Note that this is feature of [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client) not the JOSDK. But it's worth to mention here. + +To automatically generate CRD manifests from your annotated Custom Resource classes, you only need to add the following +dependencies to your project: + +```xml + + + io.fabric8 + crd-generator-apt + provided + +``` + +The CRD will be generated in `target/classes/META-INF/fabric8` (or in `target/test-classes/META-INF/fabric8`, if you use +the `test` scope) with the CRD name suffixed by the generated spec version. For example, a CR using +the `java-operator-sdk.io` group with a `mycrs` plural form will result in 2 files: + +- `mycrs.java-operator-sdk.io-v1.yml` +- `mycrs.java-operator-sdk.io-v1beta1.yml` + +**NOTE:** +> Quarkus users using the `quarkus-operator-sdk` extension do not need to add any extra dependency to get their CRD generated as this is handled by the extension itself. diff --git a/docs/documentation/getting-started.md b/docs/documentation/getting-started.md index 649d454a45..72a06df93c 100644 --- a/docs/documentation/getting-started.md +++ b/docs/documentation/getting-started.md @@ -20,7 +20,8 @@ You can read about the common problems what is this operator framework is solvin ## Getting Started The easiest way to get started with SDK is start [minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/) and -execute one of our [examples](https://github.com/java-operator-sdk/samples/tree/main/mysql-schema) +execute one of our [examples](https://github.com/java-operator-sdk/samples/tree/main/mysql-schema). +There is a dedicated page to describe how to [use samples](/docs/using-samples). Here are the main steps to develop the code and deploy the operator to a Kubernetes cluster. A more detailed and specific version can be found under `samples/mysql-schema/README.md`. From b6f389407106c8c0041814e3d6eb9542779ccac3 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 14 Jan 2022 17:26:54 +0100 Subject: [PATCH 0238/1608] docs: readme improvement --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f6f31e357f..c1dcc21e67 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,15 @@ See the [contribution](https://javaoperatorsdk.io/docs/contributing) guide on th ## What is Java Operator SDK Java Operator SDK is a higher level framework and related tooling to support writing Kubernetes Operators in Java. -It makes it use to utilize best practices and patters of an Operator development. Features include: +It makes it easy to implement best practices and patters for an Operator. Features include: -* Framework for handling Kubernetes API events -* Automatic registration of Custom Resource watches -* Retry action on failure -* Smart event scheduling (only handle the latest event for the same resource) +* Optimal handling Kubernetes API events +* Automatic Registration of Custom Resource Watches +* Automatic Retries +* Smart event scheduling * Handling Observed Generations automatically -* Support For Intelligent Error Handling +* Easy to use Error Handling +* ... and everything that a batteries included framework needs For all features and their usage see the [related section on the website](https://javaoperatorsdk.io/docs/features). From 220bf8589bc47f21f85552b79999a024058ff353 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 14 Jan 2022 19:38:01 +0100 Subject: [PATCH 0239/1608] docs: small fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c1dcc21e67..d1e58b580d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![Java CI with Maven](https://github.com/java-operator-sdk/java-operator-sdk/workflows/Java%20CI%20with%20Maven/badge.svg) [![Discord](https://img.shields.io/discord/723455000604573736.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.com/channels/723455000604573736) -#Build Kubernetes Operators in Java Without Hassle +# Build Kubernetes Operators in Java Without Hassle ## Documentation @@ -26,7 +26,7 @@ Java Operator SDK is a higher level framework and related tooling to support wri It makes it easy to implement best practices and patters for an Operator. Features include: * Optimal handling Kubernetes API events -* Automatic Registration of Custom Resource Watches +* Handling dependent resources, related events, caching. * Automatic Retries * Smart event scheduling * Handling Observed Generations automatically From 9754146d2b2efb741506f3c5e87ba53081105f36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jan 2022 09:11:36 +0100 Subject: [PATCH 0240/1608] chore(deps): bump micrometer-core from 1.8.1 to 1.8.2 (#838) Bumps [micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.8.1 to 1.8.2. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.8.1...v1.8.2) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index db0d6dc66c..00c714b6b5 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 3.22.0 4.1.1 2.6.2 - 1.8.1 + 1.8.2 2.11 3.9.0 From e7fd79968a238d7e0acc446d949b83a06cea17b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20CROCQUESEL?= <88554524+scrocquesel@users.noreply.github.com> Date: Mon, 17 Jan 2022 09:30:51 +0100 Subject: [PATCH 0241/1608] fix(micrometer): fix NPE when reconciling already marked events (#837) Refs: java-operator-sdk/java-operator-sdk#836 --- .../monitoring/micrometer/MicrometerMetrics.java | 11 +++++++---- .../processing/event/EventProcessor.java | 16 ++++++++++++++++ .../processing/event/EventProcessorTest.java | 7 ++++++- 3 files changed, 29 insertions(+), 5 deletions(-) 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 ebe193b101..d08f1c7669 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 @@ -4,6 +4,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; @@ -57,11 +58,13 @@ public void cleanupDoneFor(ResourceID customResourceUid) { incrementCounter(customResourceUid, "events.delete"); } - public void reconcileCustomResource(ResourceID resourceID, - RetryInfo retryInfo) { + public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfoNullable) { + Optional retryInfo = Optional.ofNullable(retryInfoNullable); incrementCounter(resourceID, RECONCILIATIONS + "started", - RECONCILIATIONS + "retries.number", "" + retryInfo.getAttemptCount(), - RECONCILIATIONS + "retries.last", "" + retryInfo.isLastAttempt()); + RECONCILIATIONS + "retries.number", + "" + retryInfo.map(RetryInfo::getAttemptCount).orElse(0), + RECONCILIATIONS + "retries.last", + "" + retryInfo.map(RetryInfo::isLastAttempt).orElse(true)); } @Override 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 6f90f38c49..3b9ecb2c82 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 @@ -81,6 +81,22 @@ class EventProcessor implements EventHandler, LifecycleAw eventSourceManager); } + EventProcessor( + ReconciliationDispatcher reconciliationDispatcher, + EventSourceManager eventSourceManager, + String relatedControllerName, + Retry retry, + Metrics metrics) { + this( + eventSourceManager.getControllerResourceEventSource().getResourceCache(), + null, + relatedControllerName, + reconciliationDispatcher, + retry, + metrics, + eventSourceManager); + } + private EventProcessor( Cache cache, ExecutorService executor, 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 01a4be8a79..b462182921 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 @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceCache; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; @@ -23,6 +24,7 @@ import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -47,6 +49,7 @@ class EventProcessorTest { private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private ControllerResourceEventSource controllerResourceEventSourceMock = mock(ControllerResourceEventSource.class); + private Metrics metricsMock = mock(Metrics.class); private EventProcessor eventProcessor; private EventProcessor eventProcessorWithRetry; @@ -276,7 +279,8 @@ public void cancelScheduleOnceEventsOnSuccessfulExecution() { public void startProcessedMarkedEventReceivedBefore() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); eventProcessor = - spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null)); + spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, + metricsMock)); when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(testCustomResource())); eventProcessor.handleEvent(new Event(crID)); @@ -285,6 +289,7 @@ public void startProcessedMarkedEventReceivedBefore() { eventProcessor.start(); verify(reconciliationDispatcherMock, timeout(100).times(1)).handleExecution(any()); + verify(metricsMock, times(1)).reconcileCustomResource(any(), isNull()); } private ResourceID eventAlreadyUnderProcessing() { From 5a7d1beeb8d825295d6c39997fbd242358d5b619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 17 Jan 2022 15:33:10 +0100 Subject: [PATCH 0242/1608] fix: config override with metrics (#840) --- .../io/javaoperatorsdk/operator/Operator.java | 4 ++-- .../api/config/ConfigurationServiceOverrider.java | 5 ++++- .../operator/processing/event/EventProcessor.java | 15 --------------- .../processing/event/EventProcessorTest.java | 5 +++-- sample-operators/mysql-schema/pom.xml | 5 +++++ .../operator/sample/MySQLSchemaOperator.java | 8 +++++++- 6 files changed, 21 insertions(+), 21 deletions(-) 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 8ece5334fd..39298c2ee0 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 @@ -105,8 +105,8 @@ public void stop() throws OperatorException { */ public void register(Reconciler reconciler) throws OperatorException { - final var defaultConfiguration = configurationService.getConfigurationFor(reconciler); - register(reconciler, defaultConfiguration); + final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); + register(reconciler, controllerConfiguration); } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index b96f510d97..c9b60d2617 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -70,7 +70,10 @@ public ConfigurationService build() { @Override public ControllerConfiguration getConfigurationFor( Reconciler reconciler) { - return original.getConfigurationFor(reconciler); + ControllerConfiguration controllerConfiguration = + original.getConfigurationFor(reconciler); + controllerConfiguration.setConfigurationService(this); + return controllerConfiguration; } @Override 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 3b9ecb2c82..33c2b7645b 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 @@ -66,21 +66,6 @@ class EventProcessor implements EventHandler, LifecycleAw eventSourceManager); } - EventProcessor( - ReconciliationDispatcher reconciliationDispatcher, - EventSourceManager eventSourceManager, - String relatedControllerName, - Retry retry) { - this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), - null, - relatedControllerName, - reconciliationDispatcher, - retry, - null, - eventSourceManager); - } - EventProcessor( ReconciliationDispatcher reconciliationDispatcher, EventSourceManager eventSourceManager, 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 b462182921..5a3f73742e 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 @@ -61,11 +61,12 @@ public void setup() { when(controllerResourceEventSourceMock.getResourceCache()).thenReturn(resourceCacheMock); eventProcessor = - spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null)); + spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, + null)); eventProcessor.start(); eventProcessorWithRetry = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", - GenericRetry.defaultLimitedExponentialRetry())); + GenericRetry.defaultLimitedExponentialRetry(), null)); eventProcessorWithRetry.start(); when(eventProcessor.retryEventSource()).thenReturn(retryTimerEventSourceMock); diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 6f021b383f..f1a3c3ddc1 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -27,6 +27,11 @@ operator-framework ${project.version} + + io.javaoperatorsdk + micrometer-support + ${project.version} + org.takes takes 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 fbb9ae2b65..ebccbd4fe5 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 @@ -14,7 +14,10 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; +import io.micrometer.core.instrument.logging.LoggingMeterRegistry; public class MySQLSchemaOperator { @@ -25,7 +28,10 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); - Operator operator = new Operator(client, DefaultConfigurationService.instance()); + Operator operator = new Operator(client, + new ConfigurationServiceOverrider(DefaultConfigurationService.instance()) + .withMetrics(new MicrometerMetrics(new LoggingMeterRegistry())) + .build()); operator.register(new MySQLSchemaReconciler(client, MySQLDbConfig.loadFromEnvironmentVars())); operator.installShutdownHook(); operator.start(); From 09180abaeb93646bd7bbe8e924fd2336e61808f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 17 Jan 2022 15:33:27 +0100 Subject: [PATCH 0243/1608] docs: fixes and improvements (#839) --- docs/documentation/faq.md | 30 ++- docs/documentation/features.md | 237 ++++++++++++++---- .../event/ReconciliationDispatcherTest.java | 3 +- 3 files changed, 224 insertions(+), 46 deletions(-) diff --git a/docs/documentation/faq.md b/docs/documentation/faq.md index fd7f2ff46b..da9de05aaa 100644 --- a/docs/documentation/faq.md +++ b/docs/documentation/faq.md @@ -5,10 +5,36 @@ layout: docs permalink: /docs/faq --- -## Q: How can I access events? +### Q: How can I access the events which triggered the Reconciliation? In the v1.* version events were exposed to `Reconciler` (in v1 called `ResourceController`). This included events (Create, Update) of the custom resource, but also events produced by Event Sources. After long discussions also with developers of golang version (controller-runtime), we decided to remove access to these events. We already advocated to not use events in the reconciliation logic, since events can be lost. Instead reconcile all the resources on every execution of reconciliation. On first this might sound a little -opinionated, but there was a sound agreement between the developers that this is the way to go. \ No newline at end of file +opinionated, but there was a sound agreement between the developers that this is the way to go. + +### Q: Can I re-schedule a reconciliation, possibly with a specific delay? +Yes, this can be done using [`UpdateControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java) and [`DeleteControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java) +, see: + +```java + @Override + public UpdateControl reconcile( + EventSourceTestCustomResource resource, Context context) { + ... + return UpdateControl.updateStatus(resource).rescheduleAfter(10, TimeUnit.SECONDS); + } +``` + +without an update: + +```java + @Override + public UpdateControl reconcile( + EventSourceTestCustomResource resource, Context context) { + ... + return UpdateControl.noUpdate().rescheduleAfter(10, TimeUnit.SECONDS); + } +``` + +Although you might consider using `EventSources`, to handle reconciliation triggering in a smarter way. \ No newline at end of file diff --git a/docs/documentation/features.md b/docs/documentation/features.md index a143316370..8edfec98e5 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -45,13 +45,13 @@ before the first reconciliation. The finalizer is added via a separate Kubernete the finalizer will be present. The subsequent event will be received, which will trigger the first reconciliation. The finalizer that is automatically added will be also removed after the `cleanup` is executed on the reconciler. -However, the removal behavior can be further customized, and can be instructed to "not remove yet" - this is useful just +However, the removal behaviour can be further customized, and can be instructed to "not remove yet" - this is useful just in some specific corner cases, when there would be a long waiting period for some dependent resource cleanup. The name of the finalizers can be specified, in case it is not, a name will be generated. Automatic finalizer handling can be turned off, so when configured no finalizer will be added or removed. -See [`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ControllerConfiguration.java) +See [`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java) annotation for more details. ### When not to Use Finalizers? @@ -66,29 +66,53 @@ When automatic finalizer handling is turned off, the `Reconciler.cleanup(...)` m case when a delete event received. So it does not make sense to implement this method and turn off finalizer at the same time. -## The `reconcile` and `cleanup` Methods of `Reconciler` +## The `reconcile` and `cleanup` Methods of [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) -The lifecycle of a custom resource can be clearly separated to two phases from a perspective of an operator. When a +The lifecycle of a custom resource can be clearly separated into two phases from the perspective of an operator. When a custom resource is created or update, or on the other hand when the custom resource is deleted - or rather marked for deletion in case a finalizer is used. -This separation related logic is automatically handled by framework. The framework will always call `reconcile` +This separation-related logic is automatically handled by the framework. The framework will always call `reconcile` method, unless the custom resource is [marked from deletion](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/#how-finalizers-work) . From the point when the custom resource is marked from deletion, only the `cleanup` method is called. If there is **no finalizer** in place (see Finalizer Support section), the `cleanup` method is **not called**. -### Using `UpdateControl` and `DeleteControl` +### Using [`UpdateControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java) and [`DeleteControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java) -These two classes are used to control the outcome or the desired behavior after the reconciliation. +These two classes are used to control the outcome or the desired behaviour after the reconciliation. The `UpdateControl` can instruct the framework to update the status sub-resource of the resource and/or re-schedule a -reconciliation with a desired time delay. Those are the typical use cases, however in some cases there it can happen -that the controller wants to update the custom resource itself (like adding annotations) or not to do any updates, which -is also supported. +reconciliation with a desired time delay. + +```java + @Override + public UpdateControl reconcile( + EventSourceTestCustomResource resource, Context context) { + ... + return UpdateControl.updateStatus(resource).rescheduleAfter(10, TimeUnit.SECONDS); + } +``` + +without an update: + +```java + @Override + public UpdateControl reconcile( + EventSourceTestCustomResource resource, Context context) { + ... + return UpdateControl.noUpdate().rescheduleAfter(10, TimeUnit.SECONDS); + } +``` + +Note, that it's not always desirable to always schedule a retry, rather to use `EventSources` to trigger the +reconciliation. + +Those are the typical use cases of resource updates, however in some cases there it can happen that the controller wants +to update the custom resource itself (like adding annotations) or not to do any updates, which is also supported. -It is also possible to update both the status and the custom resource with `updateCustomResourceAndStatus` method. In +It is also possible to update both the status and the custom resource with the `updateCustomResourceAndStatus` method. In this case first the custom resource is updated then the status in two separate requests to K8S API. Always update the custom resource with `UpdateControl`, not with the actual kubernetes client if possible. @@ -96,15 +120,22 @@ Always update the custom resource with `UpdateControl`, not with the actual kube On resource updates there is always an optimistic version control in place, to make sure that another update is not overwritten (by setting `resourceVersion` ) . -The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent -resource are cleaned up in `cleanup` implementation. +The `DeleteControl` typically instructs the framework to remove the finalizer after the dependent resource are cleaned +up in `cleanup` implementation. + +```java + +public DeleteControl cleanup(MyCustomResource customResource, Context context) { + ... + return DeleteControl.defaultDelete(); +} + +``` -However, there is a possibility to not remove the finalizer, this allows to clean up the resources -in a more async way, mostly for the cases when there is a long waiting period after a delete -operation is initiated. Note that in this case you might want to either schedule a timed event to -make sure -`cleanup` is executed again or use event sources get notified about the state changes of a deleted -resource. +However, there is a possibility to not remove the finalizer, this allows to clean up the resources in a more async way, +mostly for the cases when there is a long waiting period after a delete operation is initiated. Note that in this case +you might want to either schedule a timed event to make sure +`cleanup` is executed again or use event sources to get notified about the state changes of a deleted resource. ## Generation Awareness and Automatic Observed Generation Handling @@ -113,6 +144,8 @@ the resource reconciled successfully by the controller. This helps the users / a resource was reconciled, but it is used to decide if a reconciliation should happen or not. Filtering events based on generation is supported by the framework and turned on by default. There are two modes. +### Primary (preferred) Mode + The first and the **preferred** one is to check after a resource event received, if the generation of the resource is larger than the `.observedGeneration` field on status. In order to have this feature working: @@ -120,10 +153,11 @@ larger than the `.observedGeneration` field on status. In order to have this fea [`ObservedGenerationAware`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java) interface. See also the [`ObservedGenerationAwareStatus`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAwareStatus.java) - which can be also extended. + which can also be extended. - The other condition is that the `CustomResource.getStatus()` method should not return `null` , but an instance of the class representing `status`. The best way to achieve this is to - override [`CustomResource.initStatus()`](https://github.com/fabric8io/kubernetes-client/blob/865e0ddf67b99f954aa55ab14e5806d53ae149ec/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/CustomResource.java#L139). + override [`CustomResource.initStatus()`](https://github.com/fabric8io/kubernetes-client/blob/865e0ddf67b99f954aa55ab14e5806d53ae149ec/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/CustomResource.java#L139) + . If these conditions are fulfilled and generation awareness not turned off, the observed generation is automatically set by the framework after the `reconcile` method is called. There is just one exception, when the reconciler returns @@ -133,8 +167,34 @@ when `UpdateControl.noUpdate()` is returned from the reconciler. See this featur the [WebPage example](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java#L5) . +```java +public class WebPageStatus extends ObservedGenerationAwareStatus { + + private String htmlConfigMap; + + ... +} +``` + +Initializing status on custom resource: + +```java +@Group("sample.javaoperatorsdk") +@Version("v1") +public class WebPage extends CustomResource + implements Namespaced { + + @Override + protected WebPageStatus initStatus() { + return new WebPageStatus(); + } +} +``` + +### The Second (Fallback) Mode + The second, fallback mode is (when the conditions from above are not met to handle the observed generation automatically -in status) to handled generation filtering in memory. Thus, if an event is received, the generation of the received +in status) to handle generation filtering in memory. Thus, if an event is received, the generation of the received resource is compared with the resource in the cache. Note that the **first approach has significant benefits** in the situation when the operator is restarted and there is @@ -148,6 +208,21 @@ Ingress,Deployment,...). Note that automatic observed generation handling is not in case adding a secondary controller for well known k8s resource, probably the observed generation should be handled by the primary controller. +See +the [integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java) +for reconciling deployments. + +```java +public class DeploymentReconciler + implements Reconciler, TestExecutionInfoProvider { + + @Override + public UpdateControl reconcile( + Deployment resource, Context context) { + ... + } +``` + ## Automatic Retries on Error When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. The @@ -158,7 +233,14 @@ retry is behavior is configurable, an implementation is provided that should cov It is possible to set a limit on the number of retries. In the [Context](https://github.com/java-operator-sdk/java-operator-sdk/blob/master/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/Context.java) object information is provided about the retry, particularly interesting is the `isLastAttempt`, since a different -behavior could be implemented bases on this flag. Like setting an error message in the status in case of a last attempt; +behavior could be implemented based on this flag. Like setting an error message in the status in case of a last attempt; + +```java + GenericRetry.defaultLimitedExponentialRetry() + .setInitialInterval(5000) + .setIntervalMultiplier(1.5D) + .setMaxAttempts(5); +``` Event if the retry reached a limit, in case of a new event is received the reconciliation would happen again, it's just won't be a result of a retry, but the new event. However, in case of an error happens also in this case, it won't @@ -180,9 +262,9 @@ public interface ErrorStatusHandler { ``` The `updateErrorStatus` method is called in case an exception is thrown from the reconciler. It is also called when -there is no retry configured, just after the reconciler execution. -In the first call the `RetryInfo.getAttemptCount()` is always zero, since it is not a result of a retry -(regardless if retry is configured or not). +there is no retry configured, just after the reconciler execution. In the first call the `RetryInfo.getAttemptCount()` +is always zero, since it is not a result of a retry +(regardless if retry is configured or not). The result of the method call is used to make a status update on the custom resource. This is always a sub-resource update request, so no update on custom resource itself (like spec of metadata) happens. Note that this update request @@ -193,7 +275,7 @@ resource after it is marked for deletion. ### Correctness and Automatic Retries -There is a possibility to turn of the automatic retries. This is not desirable, unless there is a very specific reason. +There is a possibility to turn off the automatic retries. This is not desirable, unless there is a very specific reason. Errors naturally happen, typically network errors can cause some temporal issues, another case is when a custom resource is updated during the reconciliation (using `kubectl` for example), in this case if an update of the custom resource from the controller (using `UpdateControl`) would fail on a conflict. The automatic retries covers these cases and will @@ -201,13 +283,6 @@ result in a reconciliation, even if normally an event would not be processed as from previous example (like if there is no generation update as a result of the change and generation filtering is turned on) -## Rescheduling Execution - -One way to implement an operator especially in simple cases is to periodically reconcile it. This is supported -explicitly by -`UpdateControl`, see method: `public UpdateControl rescheduleAfter(long delay, TimeUnit timeUnit)`. This would -schedule a reconciliation for the future. - ## Retry and Rescheduling and Event Handling Common Behavior Retry, reschedule and standard event processing forms a relatively complex system, where these functionalities are not @@ -249,20 +324,93 @@ Typically, when we work with Kubernetes (but possibly with others), we manage th true also for Event Sources. For example if we watch for changes of a Kubernetes Deployment object in the InformerEventSource, we always receive the whole object from the Kubernetes API. Later when we try to reconcile in the controller (not using events) we would like to check the state of this deployment (but also other dependent resources), -we could read the object again from Kubernetes API. However since we watch for the changes we know that we always +we could read the object again from Kubernetes API. However since we watch for the changes, we know that we always receive the most up-to-date version in the Event Source. So naturally, what we can do is cache the latest received -objects (in the Event Source) and read it from there if needed. +objects (in the Event Source) and read it from there if needed. This is the preferred way, since it reduces the number +of requests to Kubernetes API server, and leads to faster reconciliation cycles. -### Implementing and EventSource +Note that when an operator starts and the first reconciliation is executed the caches are already populated for example +for `InformerEventSource`. Currently, this is not true however for `PerResourceEventSource`, where the cache might or +might not be populated. To handle this situation elegantly methods are provided which checks the object in cache, if +not found tries to get it from the supplier. See related [method](https://github.com/java-operator-sdk/java-operator-sdk/blob/e7fd79968a238d7e0acc446d949b83a06cea17b5/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java#L145) +. + +### Registering Event Sources + +To register event sources `Reconciler` has to +implement [`EventSourceInitializer`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java) +interface and init a list of event sources to register. The easiest way to see it is +on [tomcat example](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java) +(irrelevant details omitted): + +```java -### Built-in Event Sources +@ControllerConfiguration +public class TomcatReconciler implements Reconciler, EventSourceInitializer { + + @Override + public List prepareEventSources(EventSourceContext context) { + SharedIndexInformer deploymentInformer = + kubernetesClient.apps() + .deployments() + .inAnyNamespace() + .withLabel("app.kubernetes.io/managed-by", "tomcat-operator") + .runnableInformer(0); + + return List.of( + new InformerEventSource<>(deploymentInformer, d -> { + var ownerReferences = d.getMetadata().getOwnerReferences(); + if (!ownerReferences.isEmpty()) { + return Set.of(new ResourceID(ownerReferences.get(0).getName(), d.getMetadata().getNamespace())); + } else { + return EMPTY_SET; + } + })); + } + ... +} +``` -1. InformerEventSource - used to get event about other K8S resources, also provides a local cache for them. -2. TimerEventSource - used to create timed events, mainly intended for internal usage. -3. CustomResourceEventSource - an event source that is automatically registered to listen to the changes of the main +In the example above an `InformerEventSource` is registered (more on this specific eventsource later). Multiple things +are going on here: + +1. An `SharedIndexInformer` (class from fabric8 Kubernetes client) is created. This will watch and produce events for + `Deployments` in every namespace, but will filter them based on label. So `Deployments` which are not managed by + `tomcat-operator` (the label is not present on them) will not trigger a reconciliation. +2. In the next step + an [InformerEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java) + is created, which wraps the `SharedIndexInformer`. In addition to that a mapping functions is provided, **this maps + the event of the watched resource (in this case `Deployment`) to the custom resources to reconcile**. Not that in + this case this is a simple task, since `Deployment` is already created with an owner reference. Therefore, + the `ResourceID` + what identifies the custom resource to reconcile is created from the owner reference. + +Note that a set of `ResourceID` is returned, this is usually just a set with one element. The possibility to specify +multiple values are there to cover some rare corner cases. If an irrelevant resource is observed, an empty set can +be returned to not reconcile any custom resource. + +### Built-in EventSources + +There are multiple event-sources provided out of the box, the following are some more central ones: + +1. [InformerEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java) - + is there to cover events for all Kubernetes resources. Provides also a cache to use during the reconciliation. + Basically no other event source required to watch Kubernetes resources. +2. [PerResourcePollingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java) - + is used to poll external API, which don't support webhooks or other event notifications. It extends the abstract + [CachingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java) + to support caching. See [MySQL Schema sample](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java) for usage. +3. [PollingEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java) + is similar to `PerResourceCachingEventSource` only it not polls a specific API separately per custom resource, but + periodically and independently of actually observed custom resources. +5. [SimpleInboundEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/SimpleInboundEventSource.java) + and [CachingInboundEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java) + is used to handle incoming events from webhooks and messaging systems. +6. [ControllerResourceEventSource](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java) - + an eventsource that is automatically registered to listen to the changes of the main resource the operation manages, it also maintains a cache of those objects that can be accessed from the Reconciler. -## Monitoring with Micrometer +More on the philosophy of the non Kubernetes API related event source see in issue [#729](https://github.com/java-operator-sdk/java-operator-sdk/issues/729). ## Contextual Info for Logging with MDC @@ -281,9 +429,12 @@ following attributes are available in most parts of reconciliation logic and dur For more information about MDC see this [link](https://www.baeldung.com/mdc-in-log4j-2-logback). +## Monitoring with Micrometer + ## Automatic generation of CRDs -Note that this is feature of [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client) not the JOSDK. But it's worth to mention here. +Note that this is feature of [Fabric8 Kubernetes Client](https://github.com/fabric8io/kubernetes-client) not the JOSDK. +But it's worth to mention here. To automatically generate CRD manifests from your annotated Custom Resource classes, you only need to add the following dependencies to your project: 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 1c43faf5f6..9da51337d3 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 @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Optional; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -309,7 +310,7 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { markForDeletion(testCustomResource); when(reconciler.cleanup(eq(testCustomResource), any())) - .thenReturn(DeleteControl.noFinalizerRemoval().rescheduleAfter(1000L)); + .thenReturn(DeleteControl.noFinalizerRemoval().rescheduleAfter(1, TimeUnit.SECONDS)); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); From d3863e18b1f51640a29985a60275f52174f9cba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 17 Jan 2022 20:36:28 +0100 Subject: [PATCH 0244/1608] feature: ResourceID from first owner reference (#841) --- .../processing/event/source/informer/Mappers.java | 12 ++++++++++++ .../operator/sample/TomcatReconciler.java | 14 ++------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index b3e0cf3b65..e3b4b7be14 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -29,6 +29,18 @@ public static PrimaryResourcesRetriever fromLabel( return fromMetadata(nameKey, namespaceKey, true); } + public static PrimaryResourcesRetriever fromOwnerReference() { + return resource -> { + var ownerReferences = resource.getMetadata().getOwnerReferences(); + if (!ownerReferences.isEmpty()) { + return Set.of(new ResourceID(ownerReferences.get(0).getName(), + resource.getMetadata().getNamespace())); + } else { + return Collections.emptySet(); + } + }; + } + private static PrimaryResourcesRetriever fromMetadata( String nameKey, String namespaceKey, boolean isLabel) { return resource -> { diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 6f91410594..bb67128f9e 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -4,7 +4,6 @@ import java.io.InputStream; import java.util.List; import java.util.Objects; -import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,12 +21,11 @@ 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.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; -import static java.util.Collections.EMPTY_SET; /** * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also @@ -52,15 +50,7 @@ public List prepareEventSources(EventSourceContext context) .runnableInformer(0); return List.of(new InformerEventSource<>( - deploymentInformer, d -> { - var ownerReferences = d.getMetadata().getOwnerReferences(); - if (!ownerReferences.isEmpty()) { - return Set.of(new ResourceID(ownerReferences.get(0).getName(), - d.getMetadata().getNamespace())); - } else { - return EMPTY_SET; - } - })); + deploymentInformer, Mappers.fromOwnerReference())); } @Override From 0aa612492eb49b2335188fdfcc32f158633e6328 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 17 Jan 2022 19:43:28 +0000 Subject: [PATCH 0245/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index fc055ea220..c8a4add4fb 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 54409af136..fbf8ebc53b 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 0339129c75..f2cea8bf99 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 0c05632099..bd3bfb661f 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 00c714b6b5..4ba30ee2f4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index f1a3c3ddc1..9cf32ef517 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index d186916abd..9436f9a0de 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index b952d03f86..859beffe23 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 9dde813564..7a2fa43f8f 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 0038730ea3..4fe1bfd9eb 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 2d1ad2923a..f8ff9c0a8e 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 9fb7f1c92c..31a4e9cac0 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index 27e5c01d70..6787681dc1 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.1-SNAPSHOT + 2.0.2-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 574ed6111621d5074979bebd54b201adea7287c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jan 2022 13:28:26 +0100 Subject: [PATCH 0246/1608] chore(deps): bump mysql-connector-java from 8.0.27 to 8.0.28 (#844) Bumps [mysql-connector-java](https://github.com/mysql/mysql-connector-j) from 8.0.27 to 8.0.28. - [Release notes](https://github.com/mysql/mysql-connector-j/releases) - [Changelog](https://github.com/mysql/mysql-connector-j/blob/release/8.0/CHANGES) - [Commits](https://github.com/mysql/mysql-connector-j/commits) --- updated-dependencies: - dependency-name: mysql:mysql-connector-java dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sample-operators/mysql-schema/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 9cf32ef517..edd6adbcec 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -40,7 +40,7 @@ mysql mysql-connector-java - 8.0.27 + 8.0.28 io.fabric8 From c43965e491bd3a092ea0fc1e2f243bb6cc4218de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 18 Jan 2022 14:01:38 +0100 Subject: [PATCH 0247/1608] docs: community meeting in the main readme (#845) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d1e58b580d..ffc36dc7c0 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ It's getting better every day! :) Join us on [Discord](https://discord.gg/DacEhAy) or feel free to ask any question on [Kubernetes Slack Operator Channel](https://kubernetes.slack.com/archives/CAW0GV7A5) +**Meet us** every Thursday (17:00 CET) at our **community meeting** on [Zoom](https://zoom.us/j/8415370125) +(Password in the Discord channel, or just ask for it there!) + ## How to Contribute See the [contribution](https://javaoperatorsdk.io/docs/contributing) guide on the website. From e5c3c713d7431c246f5f4775594dbd45324447eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 19 Jan 2022 12:29:05 +0100 Subject: [PATCH 0248/1608] feature: add from first reference to ResourceID too (#846) --- .../operator/processing/event/ResourceID.java | 10 ++++++++++ .../processing/event/source/informer/Mappers.java | 11 ++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java index 38c9055ff1..27634f4b76 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java @@ -13,6 +13,16 @@ public static ResourceID fromResource(HasMetadata resource) { resource.getMetadata().getNamespace()); } + public static Optional fromFirstOwnerReference(HasMetadata resource) { + var ownerReferences = resource.getMetadata().getOwnerReferences(); + if (!ownerReferences.isEmpty()) { + return Optional.of(new ResourceID(ownerReferences.get(0).getName(), + resource.getMetadata().getNamespace())); + } else { + return Optional.empty(); + } + } + private final String name; private final String namespace; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index e3b4b7be14..053528e0ff 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -30,15 +30,8 @@ public static PrimaryResourcesRetriever fromLabel( } public static PrimaryResourcesRetriever fromOwnerReference() { - return resource -> { - var ownerReferences = resource.getMetadata().getOwnerReferences(); - if (!ownerReferences.isEmpty()) { - return Set.of(new ResourceID(ownerReferences.get(0).getName(), - resource.getMetadata().getNamespace())); - } else { - return Collections.emptySet(); - } - }; + return resource -> ResourceID.fromFirstOwnerReference(resource).map(Set::of) + .orElse(Collections.emptySet()); } private static PrimaryResourcesRetriever fromMetadata( From ba03d9eacf2772329e7a7703035990519ebb17ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jan 2022 09:06:00 +0100 Subject: [PATCH 0249/1608] chore(deps): bump jib-maven-plugin from 3.1.4 to 3.2.0 (#849) Bumps [jib-maven-plugin](https://github.com/GoogleContainerTools/jib) from 3.1.4 to 3.2.0. - [Release notes](https://github.com/GoogleContainerTools/jib/releases) - [Commits](https://github.com/GoogleContainerTools/jib/commits) --- updated-dependencies: - dependency-name: com.google.cloud.tools:jib-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index edd6adbcec..a3bcd79192 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -18,7 +18,7 @@ 11 11 - 3.1.4 + 3.2.0 diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 859beffe23..3bfc8382e6 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -18,7 +18,7 @@ 11 11 - 3.1.4 + 3.2.0 diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 7a2fa43f8f..a2579c1575 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -18,7 +18,7 @@ 11 11 - 2.7.1 + 3.2.0 From ade382e80fd461a6cde833b79bc3894b7c08e7d8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 20 Jan 2022 10:22:17 +0000 Subject: [PATCH 0250/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index c8a4add4fb..9ca97c7186 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index fbf8ebc53b..a615428790 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index f2cea8bf99..69a163b7d7 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index bd3bfb661f..fd124ea222 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 4ba30ee2f4..4cd3bbd919 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index a3bcd79192..58d1f3155a 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 9436f9a0de..9877aaef0d 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 3bfc8382e6..ffcd7038e7 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index a2579c1575..90522733af 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 4fe1bfd9eb..db2926c74b 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index f8ff9c0a8e..3dcdb530c7 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 31a4e9cac0..87ccbbf48d 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index 6787681dc1..b652916960 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.2-SNAPSHOT + 2.0.3-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 979b83a9b8cc4ab4bfb7cde334e085ddb67d568a Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Thu, 20 Jan 2022 17:01:43 +0000 Subject: [PATCH 0251/1608] More principled way to wait for the CRD (#852) --- .../operator/junit/OperatorExtension.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 03208422de..5941975a71 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -169,12 +169,9 @@ protected void before(ExtensionContext context) { } try (InputStream is = getClass().getResourceAsStream(path)) { - kubernetesClient.load(is).createOrReplace(); - // this fixes an issue with CRD registration, integration tests were failing, since the CRD - // was not found yet - // when the operator started. This seems to be fixing this issue (maybe a problem with - // minikube?) - Thread.sleep(2000); + final var crd = kubernetesClient.load(is); + crd.createOrReplace(); + crd.waitUntilReady(2, TimeUnit.SECONDS); LOGGER.debug("Applied CRD with name: {}", config.getResourceTypeName()); } catch (Exception ex) { throw new IllegalStateException("Cannot apply CRD yaml: " + path, ex); From 370e60d56b06c8ea9e999355ef631659ebcd29b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jan 2022 08:36:22 +0100 Subject: [PATCH 0252/1608] chore(deps): bump spring-boot.version from 2.6.2 to 2.6.3 (#856) Bumps `spring-boot.version` from 2.6.2 to 2.6.3. Updates `spring-boot-dependencies` from 2.6.2 to 2.6.3 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.2...v2.6.3) Updates `spring-boot-maven-plugin` from 2.6.2 to 2.6.3 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.2...v2.6.3) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4cd3bbd919..22bc63c36b 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ 1.13.0 3.22.0 4.1.1 - 2.6.2 + 2.6.3 1.8.2 2.11 From b006d98a7fd1b870cd9570b66d28cfd5bd1e0f0b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 21 Jan 2022 09:09:18 +0100 Subject: [PATCH 0253/1608] fix: remove CustomFilter preventing WebappReconciler from working (#855) Fixes #854 --- .../operator/sample/CustomFilter.java | 13 ------------- .../operator/sample/WebappReconciler.java | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/CustomFilter.java diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/CustomFilter.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/CustomFilter.java deleted file mode 100644 index 4b8ec620b7..0000000000 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/CustomFilter.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; - -public class CustomFilter implements ResourceEventFilter { - @Override - public boolean acceptChange(ControllerConfiguration configuration, HasMetadata oldResource, - HasMetadata newResource) { - return false; - } -} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 66e9a68def..0f785feb02 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -28,7 +28,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -@ControllerConfiguration(eventFilters = {CustomFilter.class}) +@ControllerConfiguration public class WebappReconciler implements Reconciler, EventSourceInitializer { private KubernetesClient kubernetesClient; From c43764261aeb1ce05d5d7db6ffd6f01c5049ea30 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 21 Jan 2022 15:41:10 +0100 Subject: [PATCH 0254/1608] chore(ci): rename snapshots workflow, add next, only keep unit tests (#859) --- ...shot-release.yml => snapshot-releases.yml} | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) rename .github/workflows/{master-snapshot-release.yml => snapshot-releases.yml} (63%) diff --git a/.github/workflows/master-snapshot-release.yml b/.github/workflows/snapshot-releases.yml similarity index 63% rename from .github/workflows/master-snapshot-release.yml rename to .github/workflows/snapshot-releases.yml index e754b85894..df593393a6 100644 --- a/.github/workflows/master-snapshot-release.yml +++ b/.github/workflows/snapshot-releases.yml @@ -8,34 +8,21 @@ concurrency: cancel-in-progress: true on: push: - branches: [ main, v1 ] + branches: [ main, v1, next ] workflow_dispatch: jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - java: [ 11, 17 ] - distribution: [ temurin ] - kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1', 'v1.23.0' ] steps: - uses: actions/checkout@v2 - name: Set up Java and Maven uses: actions/setup-java@v2 with: - distribution: ${{ matrix.distribution }} - java-version: ${{ matrix.java }} + distribution: temurin + java-version: 11 cache: 'maven' - name: Run unit tests run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml - - name: Set up Minikube - uses: manusa/actions-setup-minikube@v2.4.3 - with: - minikube version: 'v1.24.0' - kubernetes version: ${{ matrix.kubernetes }} - driver: 'docker' - - name: Run integration tests - run: ./mvnw ${MAVEN_ARGS} -B package -P no-unit-tests --file pom.xml release-snapshot: runs-on: ubuntu-latest needs: test From c4d69f09b6fe0ef37602a945661dfb0d7f5bcd8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jan 2022 09:49:59 +0100 Subject: [PATCH 0255/1608] chore(deps-dev): bump mockito-core from 4.2.0 to 4.3.0 (#865) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.2.0...v4.3.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 22bc63c36b..632a887d95 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 5.11.2 1.7.33 2.17.1 - 4.2.0 + 4.3.0 3.12.0 1.0.1 0.19 From 8220d37f078a121c9a6e1a9d2a43ecc3991018c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jan 2022 09:50:17 +0100 Subject: [PATCH 0256/1608] chore(deps): bump kubernetes-client-bom from 5.11.2 to 5.12.0 (#864) Bumps [kubernetes-client-bom](https://github.com/fabric8io/kubernetes-client) from 5.11.2 to 5.12.0. - [Release notes](https://github.com/fabric8io/kubernetes-client/releases) - [Changelog](https://github.com/fabric8io/kubernetes-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/fabric8io/kubernetes-client/compare/v5.11.2...v5.12.0) --- updated-dependencies: - dependency-name: io.fabric8:kubernetes-client-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 632a887d95..d036c50aa3 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ https://sonarcloud.io 5.8.2 - 5.11.2 + 5.12.0 1.7.33 2.17.1 4.3.0 From 364813d8fb5ed689ddbf0bba34f370c7146226c3 Mon Sep 17 00:00:00 2001 From: sullis Date: Tue, 25 Jan 2022 05:01:16 -0800 Subject: [PATCH 0257/1608] chore: use official Maven Wrapper (#861) https://maven.apache.org/wrapper/index.html https://github.com/apache/maven-wrapper https://twitter.com/jvanzyl/status/1472199110866481159 --- .mvn/wrapper/maven-wrapper.properties | 20 +- mvnw | 18 +- mvnw.cmd | 370 +++++++++++++------------- 3 files changed, 218 insertions(+), 190 deletions(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index abd303b673..8c79a83ae4 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,18 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/mvnw b/mvnw index 41c0f0c23d..5643201c7d 100755 --- a/mvnw +++ b/mvnw @@ -36,6 +36,10 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi @@ -145,7 +149,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`which java`" + JAVACMD="`\\unset -f command; \\command -v java`" fi fi @@ -212,9 +216,9 @@ else echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; @@ -233,9 +237,9 @@ else echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then @@ -305,6 +309,8 @@ WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 86115719e5..23b7079a3d 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,182 +1,188 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% From 3ec0a1e4926e238b0ae10a262b4b8b0fdf0947db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jan 2022 08:03:19 +0100 Subject: [PATCH 0258/1608] chore(deps): bump slf4j-api from 1.7.33 to 1.7.35 (#869) Bumps [slf4j-api](https://github.com/qos-ch/slf4j) from 1.7.33 to 1.7.35. - [Release notes](https://github.com/qos-ch/slf4j/releases) - [Commits](https://github.com/qos-ch/slf4j/compare/v_1.7.33...v_1.7.35) --- updated-dependencies: - dependency-name: org.slf4j:slf4j-api dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d036c50aa3..576836287f 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 5.8.2 5.12.0 - 1.7.33 + 1.7.35 2.17.1 4.3.0 3.12.0 From 2fb005315533964dea61343d1ff4b2db383808a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jan 2022 08:03:44 +0100 Subject: [PATCH 0259/1608] chore(deps-dev): bump mockito-core from 4.3.0 to 4.3.1 (#868) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.3.0 to 4.3.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.3.0...v4.3.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 576836287f..a520155deb 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 5.12.0 1.7.35 2.17.1 - 4.3.0 + 4.3.1 3.12.0 1.0.1 0.19 From aa82130af51ed03a95a0c253ae4de9855e3a8eeb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 24 Dec 2021 15:14:41 +0100 Subject: [PATCH 0260/1608] feat: support for declarative management of dependent resources - Kubernetes-native resources are automatically handled without users having to deal with explicit event sources - Reconcilers and event sources can exchange information via the `EventSourceContext` - Declarative management is completely opt-in and can cohabit with "traditional" handling i.e. it's possible to have only part of the dependents being declared via annotations and others explicitly handled (though from a maintenance perspective it's better to stick to one approach or the other) --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- .../api/config/ControllerConfiguration.java | 83 ++------ .../ControllerConfigurationOverrider.java | 3 +- .../DefaultControllerConfiguration.java | 14 +- .../config/DefaultResourceConfiguration.java | 65 +++++++ .../operator/api/config/Dependent.java | 8 + .../api/config/DependentResource.java | 15 ++ .../api/config/ExecutorServiceManager.java | 3 + .../api/config/KubernetesDependent.java | 34 ++++ .../api/config/ResourceConfiguration.java | 78 ++++++++ .../operator/api/config/Utils.java | 6 + .../api/reconciler/AttributeHolder.java | 12 ++ .../operator/api/reconciler/Context.java | 9 +- .../api/reconciler/ContextInitializer.java | 7 + .../reconciler/ControllerConfiguration.java | 12 ++ .../api/reconciler/DefaultContext.java | 2 +- .../api/reconciler/EventSourceContext.java | 4 +- .../EventSourceContextInjector.java | 5 + .../api/reconciler/MapAttributeHolder.java | 28 +++ .../api/reconciler/dependent/Builder.java | 9 + .../api/reconciler/dependent/Cleaner.java | 9 + .../DependentResourceController.java | 87 +++++++++ .../DependentResourceControllerFactory.java | 14 ++ ...KubernetesDependentResourceController.java | 74 ++++++++ .../api/reconciler/dependent/Persister.java | 11 ++ .../api/reconciler/dependent/Updater.java | 10 + .../operator/processing/Controller.java | 52 +++-- .../dependent/DependentResourceManager.java | 140 ++++++++++++++ .../processing/event/EventSourceManager.java | 41 ++-- .../event/source/AbstractEventSource.java | 2 +- .../event/source/CachingEventSource.java | 8 +- .../event/source/EventSourceContextAware.java | 8 + .../ControllerResourceEventSource.java | 106 ++--------- .../informer/InformerConfiguration.java | 132 +++++++++++++ .../source/informer/InformerEventSource.java | 159 +++++----------- .../source/informer/InformerManager.java | 144 ++++++++++++++ .../informer/InformerResourceCache.java | 64 +++++++ .../source/informer/InformerWrapper.java | 84 ++++++++ .../informer/ManagedInformerEventSource.java | 43 +++++ .../event/source/informer/Mappers.java | 2 + .../operator/ControllerManagerTest.java | 9 +- .../operator/MockKubernetesClient.java | 52 +++++ .../operator/OperatorTest.java | 3 +- .../config/ControllerConfigurationTest.java | 3 + .../operator/processing/ControllerTest.java | 20 +- .../event/EventSourceManagerTest.java | 6 +- .../event/ReconciliationDispatcherTest.java | 5 +- .../source/CustomResourceSelectorTest.java | 3 + .../event/source/ResourceEventFilterTest.java | 19 +- .../ControllerResourceEventSourceTest.java | 15 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- .../runtime/AnnotationConfiguration.java | 65 ++++++- ...formerEventSourceTestCustomReconciler.java | 47 ++--- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- .../operator/sample/MySQLSchemaOperator.java | 3 +- .../sample/MySQLSchemaReconciler.java | 179 ++++++++---------- .../sample/SchemaDependentResource.java | 88 +++++++++ .../operator/sample/schema/SchemaService.java | 4 +- .../sample/MySQLSchemaOperatorE2E.java | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- .../sample/DeploymentDependentResource.java | 52 +++++ .../sample/ServiceDependentResource.java | 28 +++ .../sample/TomcatDependentResource.java | 41 ++++ .../operator/sample/TomcatOperator.java | 2 +- .../operator/sample/TomcatReconciler.java | 134 ++----------- .../operator/sample/WebappReconciler.java | 51 +++-- .../operator/sample/deployment.yaml | 5 - .../operator/sample/service.yaml | 5 - .../operator/sample/TomcatOperatorE2E.java | 2 +- sample-operators/webpage/pom.xml | 3 +- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 78 files changed, 1793 insertions(+), 664 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerResourceCache.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 9ca97c7186..73c8e7ed7d 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index a615428790..a1804086d6 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT ../pom.xml 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 b85df49eba..d4739f8267 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 @@ -1,103 +1,36 @@ package io.javaoperatorsdk.operator.api.config; -import java.lang.reflect.ParameterizedType; import java.util.Collections; -import java.util.Set; +import java.util.List; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceControllerFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -public interface ControllerConfiguration { +@SuppressWarnings("rawtypes") +public interface ControllerConfiguration extends ResourceConfiguration { default String getName() { return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); } - default String getResourceTypeName() { - return ReconcilerUtils.getResourceTypeName(getResourceClass()); - } - default String getFinalizer() { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } - /** - * Retrieves the label selector that is used to filter which custom resources are actually watched - * by the associated controller. See - * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on - * syntax. - * - * @return the label selector filtering watched custom resources - */ - default String getLabelSelector() { - return null; - } - default boolean isGenerationAware() { return true; } - default Class getResourceClass() { - ParameterizedType type = (ParameterizedType) getClass().getGenericInterfaces()[0]; - return (Class) type.getActualTypeArguments()[0]; - } - String getAssociatedReconcilerClassName(); - default Set getNamespaces() { - return Collections.emptySet(); - } - - default boolean watchAllNamespaces() { - return allNamespacesWatched(getNamespaces()); - } - - static boolean allNamespacesWatched(Set namespaces) { - return namespaces == null || namespaces.isEmpty(); - } - - default boolean watchCurrentNamespace() { - return currentNamespaceWatched(getNamespaces()); - } - - static boolean currentNamespaceWatched(Set namespaces) { - return namespaces != null - && namespaces.size() == 1 - && namespaces.contains( - Constants.WATCH_CURRENT_NAMESPACE); - } - - /** - * 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 - * watch the current namespace only. - * - * @return a Set of namespace names the associated controller will watch - */ - default Set getEffectiveNamespaces() { - var targetNamespaces = getNamespaces(); - if (watchCurrentNamespace()) { - final var parent = getConfigurationService(); - if (parent == null) { - throw new IllegalStateException( - "Parent ConfigurationService must be set before calling this method"); - } - targetNamespaces = Collections.singleton(parent.getClientConfiguration().getNamespace()); - } - return targetNamespaces; - } - default RetryConfiguration getRetryConfiguration() { return RetryConfiguration.DEFAULT; } - ConfigurationService getConfigurationService(); - - default void setConfigurationService(ConfigurationService service) {} - default boolean useFinalizer() { return !Constants.NO_FINALIZER .equals(getFinalizer()); @@ -114,4 +47,12 @@ default boolean useFinalizer() { default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } + + default List getDependentResources() { + return Collections.emptyList(); + } + + default DependentResourceControllerFactory dependentFactory() { + return new DependentResourceControllerFactory<>() {}; + } } 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 e8e2ef1162..fee1212deb 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 @@ -86,7 +86,8 @@ public ControllerConfiguration build() { labelSelector, customResourcePredicate, original.getResourceClass(), - original.getConfigurationService()); + original.getConfigurationService(), + original.getDependentResources()); } 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 index 860152745b..bc10002ebd 100644 --- 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 @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.config; import java.util.Collections; +import java.util.List; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -20,8 +21,10 @@ public class DefaultControllerConfiguration private final String labelSelector; private final ResourceEventFilter resourceEventFilter; private final Class resourceClass; + private final List dependents; private ConfigurationService service; + // NOSONAR constructor is meant to provide all information public DefaultControllerConfiguration( String associatedControllerClassName, String name, @@ -33,7 +36,8 @@ public DefaultControllerConfiguration( String labelSelector, ResourceEventFilter resourceEventFilter, Class resourceClass, - ConfigurationService service) { + ConfigurationService service, + List dependents) { this.associatedControllerClassName = associatedControllerClassName; this.name = name; this.crdName = crdName; @@ -52,6 +56,7 @@ public DefaultControllerConfiguration( resourceClass == null ? ControllerConfiguration.super.getResourceClass() : resourceClass; setConfigurationService(service); + this.dependents = dependents != null ? dependents : Collections.emptyList(); } @Override @@ -102,7 +107,7 @@ public ConfigurationService getConfigurationService() { @Override public void setConfigurationService(ConfigurationService service) { if (this.service != null) { - throw new RuntimeException("A ConfigurationService is already associated with '" + name + throw new IllegalStateException("A ConfigurationService is already associated with '" + name + "' ControllerConfiguration. Cannot change it once set!"); } this.service = service; @@ -122,4 +127,9 @@ public Class getResourceClass() { public ResourceEventFilter getEventFilter() { return resourceEventFilter; } + + @Override + public List getDependentResources() { + return dependents; + } } 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 new file mode 100644 index 0000000000..d7b5f76815 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DefaultResourceConfiguration.java @@ -0,0 +1,65 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.Collections; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class DefaultResourceConfiguration + implements ResourceConfiguration { + + private final String labelSelector; + private final Set namespaces; + private final boolean watchAllNamespaces; + private final Class resourceClass; + private ConfigurationService service; + + public DefaultResourceConfiguration(String labelSelector, Class resourceClass, + String... namespaces) { + this(labelSelector, resourceClass, + namespaces != null ? Set.of(namespaces) : Collections.emptySet()); + } + + public DefaultResourceConfiguration(String labelSelector, Class resourceClass, + Set namespaces) { + this.labelSelector = labelSelector; + this.resourceClass = resourceClass; + this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); + this.watchAllNamespaces = this.namespaces.isEmpty(); + } + + @Override + public String getResourceTypeName() { + return ResourceConfiguration.super.getResourceTypeName(); + } + + @Override + public String getLabelSelector() { + return labelSelector; + } + + @Override + public Set getNamespaces() { + return namespaces; + } + + @Override + public boolean watchAllNamespaces() { + return watchAllNamespaces; + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public Class getResourceClass() { + return resourceClass; + } + + @Override + public void setConfigurationService(ConfigurationService service) { + this.service = service; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java new file mode 100644 index 0000000000..ed013fafb0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.api.config; + +public @interface Dependent { + + Class resourceType(); + + Class type(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java new file mode 100644 index 0000000000..f4d5df89fb --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public interface DependentResource { + default EventSource initEventSource(EventSourceContext

context) { + throw new IllegalStateException("Must be implemented if not automatically provided by the SDK"); + }; + + default Class resourceType() { + return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index dfe64d97d0..254a539ac3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -36,6 +36,9 @@ public static void init(ConfigurationService configuration) { log.debug("Initialized ExecutorServiceManager executor: {}, timeout: {}", configuration.getExecutorService().getClass(), configuration.getTerminationTimeoutSeconds()); + log.debug("Initialized ExecutorServiceManager executor: {}, timeout: {}", + configuration.getExecutorService().getClass(), + configuration.getTerminationTimeoutSeconds()); } else { log.debug("Already started, reusing already setup instance!"); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java new file mode 100644 index 0000000000..587fd58f85 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; + +@Target({ElementType.TYPE}) +public @interface KubernetesDependent { + + boolean OWNED_DEFAULT = true; + boolean SKIP_UPDATE_DEFAULT = true; + + boolean owned() default OWNED_DEFAULT; + + boolean skipUpdateIfUnchanged() default SKIP_UPDATE_DEFAULT; + + /** + * Specified which namespaces this Controller monitors for custom resources events. If no + * namespace is specified then the controller will monitor all namespaces by default. + * + * @return the list of namespaces this controller monitors + */ + String[] namespaces() default {}; + + /** + * Optional label selector used to identify the set of custom resources the controller will acc + * upon. The label selector can be made of multiple comma separated requirements that acts as a + * logical AND operator. + * + * @return the label selector + */ + String labelSelector() default EMPTY_STRING; +} 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 new file mode 100644 index 0000000000..1e1d58b917 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java @@ -0,0 +1,78 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.Collections; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.Constants; + +public interface ResourceConfiguration { + + default String getResourceTypeName() { + return ReconcilerUtils.getResourceTypeName(getResourceClass()); + } + + /** + * Retrieves the label selector that is used to filter which resources are actually watched by the + * associated event source. See + * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ for more details on + * syntax. + * + * @return the label selector filtering watched resources + */ + default String getLabelSelector() { + return null; + } + + @SuppressWarnings("unchecked") + default Class getResourceClass() { + return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); + } + + default Set getNamespaces() { + return Collections.emptySet(); + } + + default boolean watchAllNamespaces() { + return allNamespacesWatched(getNamespaces()); + } + + static boolean allNamespacesWatched(Set namespaces) { + return namespaces == null || namespaces.isEmpty(); + } + + default boolean watchCurrentNamespace() { + return currentNamespaceWatched(getNamespaces()); + } + + static boolean currentNamespaceWatched(Set namespaces) { + return namespaces != null + && namespaces.size() == 1 + && namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE); + } + + /** + * 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 + * watch the current namespace only. + * + * @return a Set of namespace names the associated controller will watch + */ + default Set getEffectiveNamespaces() { + var targetNamespaces = getNamespaces(); + if (watchCurrentNamespace()) { + final var parent = getConfigurationService(); + if (parent == null) { + throw new IllegalStateException( + "Parent ConfigurationService must be set before calling this method"); + } + targetNamespaces = Collections.singleton(parent.getClientConfiguration().getNamespace()); + } + return targetNamespaces; + } + + ConfigurationService getConfigurationService(); + + void setConfigurationService(ConfigurationService service); +} 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 b36c0468cd..3944cd3ecc 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 @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.config; import java.io.IOException; +import java.lang.reflect.ParameterizedType; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; @@ -67,4 +68,9 @@ public static boolean shouldCheckCRDAndValidateLocalModel() { public static boolean debugThreadPool() { return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); } + + public static Class getFirstTypeArgumentFromInterface(Class clazz) { + ParameterizedType type = (ParameterizedType) clazz.getGenericInterfaces()[0]; + return (Class) type.getActualTypeArguments()[0]; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java new file mode 100644 index 0000000000..8fee3c2557 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.Optional; + +public interface AttributeHolder { + + Optional get(Object key, Class expectedType); + + T getMandatory(Object key, Class expectedType); + + Optional put(Object key, Object value); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index d697325219..6870fef80e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -2,7 +2,7 @@ import java.util.Optional; -public interface Context { +public interface Context extends AttributeHolder { Optional getRetryInfo(); @@ -11,4 +11,11 @@ default Optional getSecondaryResource(Class expectedType) { } Optional getSecondaryResource(Class expectedType, String eventSourceName); + + @Override + default T getMandatory(Object key, Class expectedType) { + return get(key, expectedType).orElseThrow(() -> new IllegalStateException( + "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + + ") is missing or not of the expected type")); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java new file mode 100644 index 0000000000..e076723d0c --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface ContextInitializer

{ + void initContext(P primary, Context context); +} 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 fad4bc8573..5870820f0f 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,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) @@ -56,4 +58,14 @@ */ @SuppressWarnings("rawtypes") Class[] eventFilters() default {}; + + /** + * Optional list of classes providing {@link DependentResourceController} implementations + * encapsulating logic to handle the associated + * {@link io.javaoperatorsdk.operator.processing.Controller}'s reconciliation of dependent + * resources + * + * @return the list of {@link DependentResourceController} implementations + */ + Dependent[] dependents() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 3d924c2753..c2553a5d57 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.Controller; -public class DefaultContext

implements Context { +public class DefaultContext

extends MapAttributeHolder implements Context { private final RetryInfo retryInfo; private final Controller

controller; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index 0a93d33d40..026af88923 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -11,7 +11,7 @@ * * @param

the type associated with the primary resource that is handled by your reconciler */ -public class EventSourceContext

{ +public class EventSourceContext

extends MapAttributeHolder { private final ResourceCache

primaryCache; private final ConfigurationService configurationService; @@ -49,7 +49,7 @@ public ConfigurationService getConfigurationService() { /** * Provides access to the {@link KubernetesClient} used by the current * {@link io.javaoperatorsdk.operator.Operator} instance. - * + * * @return the {@link KubernetesClient} used by the current * {@link io.javaoperatorsdk.operator.Operator} instance. */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java new file mode 100644 index 0000000000..af57fe1b32 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +public interface EventSourceContextInjector { + void injectInto(EventSourceContext context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java new file mode 100644 index 0000000000..aee5ee7e54 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class MapAttributeHolder { + + private final ConcurrentHashMap attributes = new ConcurrentHashMap(); + + public Optional get(Object key, Class expectedType) { + return Optional.ofNullable(attributes.get(key)) + .filter(expectedType::isInstance) + .map(expectedType::cast); + } + + public Optional put(Object key, Object value) { + if (value == null) { + return Optional.ofNullable(attributes.remove(key)); + } + return Optional.ofNullable(attributes.put(key, value)); + } + + public T getMandatory(Object key, Class expectedType) { + return get(key, expectedType).orElseThrow(() -> new IllegalStateException( + "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + + ") is missing or not of the expected type")); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java new file mode 100644 index 0000000000..4dfaf91e64 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@FunctionalInterface +public interface Builder { + R buildFor(P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java new file mode 100644 index 0000000000..a6a3d691be --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface Cleaner { + + void delete(R fetched, P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java new file mode 100644 index 0000000000..043aec2f53 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java @@ -0,0 +1,87 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public class DependentResourceController + implements DependentResource, Builder, Updater, Persister, + Cleaner { + + private final Builder builder; + private final Updater updater; + private final Cleaner cleaner; + private final Persister persister; + private final DependentResource delegate; + + @SuppressWarnings("unchecked") + public DependentResourceController(DependentResource delegate) { + this.delegate = delegate; + builder = (delegate instanceof Builder) ? (Builder) delegate : null; + updater = (delegate instanceof Updater) ? (Updater) delegate : null; + cleaner = (delegate instanceof Cleaner) ? (Cleaner) delegate : null; + persister = initPersister(delegate); + } + + @SuppressWarnings("unchecked") + protected Persister initPersister(DependentResource delegate) { + if (delegate instanceof Persister) { + return (Persister) delegate; + } else { + throw new IllegalArgumentException( + "DependentResource '" + delegate.getClass().getName() + "' must implement Persister"); + } + } + + public String descriptionFor(R resource) { + return resource.toString(); + } + + @Override + public R buildFor(P primary, Context context) { + return builder.buildFor(primary, context); + } + + @Override + public R update(R fetched, P primary, Context context) { + return updater.update(fetched, primary, context); + } + + @Override + public void delete(R fetched, P primary, Context context) { + cleaner.delete(fetched, primary, context); + } + + public Class getResourceType() { + return delegate.resourceType(); + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + return delegate.initEventSource(context); + } + + public boolean creatable() { + return builder != null; + } + + public boolean updatable() { + return updater != null; + } + + public boolean deletable() { + return cleaner != null; + } + + @Override + public void createOrReplace(R dependentResource, Context context) { + persister.createOrReplace(dependentResource, context); + } + + @Override + public R getFor(P primary, Context context) { + return persister.getFor(primary, context); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java new file mode 100644 index 0000000000..f814cd7a1a --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.DependentResource; + +public interface DependentResourceControllerFactory

{ + + default DependentResourceController from(DependentResource dependent) { + // todo: this needs to be cleaned-up / redesigned + return dependent instanceof DependentResourceController + ? (DependentResourceController) dependent + : new DependentResourceController<>(dependent); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java new file mode 100644 index 0000000000..da11c4f938 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java @@ -0,0 +1,74 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +public class KubernetesDependentResourceController + extends DependentResourceController { + private final InformerConfiguration configuration; + private final boolean owned; + private KubernetesClient client; + private InformerEventSource informer; + + + public KubernetesDependentResourceController(DependentResource delegate, + InformerConfiguration configuration, boolean owned) { + super(delegate); + // todo: check if we can validate that types actually match properly + final var associatedPrimaries = + (delegate instanceof PrimaryResourcesRetriever) + ? (PrimaryResourcesRetriever) delegate + : configuration.getPrimaryResourcesRetriever(); + final var associatedSecondary = + (delegate instanceof AssociatedSecondaryResourceIdentifier) + ? (AssociatedSecondaryResourceIdentifier

) delegate + : configuration.getAssociatedResourceIdentifier(); + + this.configuration = InformerConfiguration.from(configuration) + .withPrimaryResourcesRetriever(associatedPrimaries) + .withAssociatedSecondaryResourceIdentifier(associatedSecondary) + .build(); + this.owned = owned; + } + + @Override + protected Persister initPersister(DependentResource delegate) { + return (delegate instanceof Persister) ? (Persister) delegate : this; + } + + @Override + public String descriptionFor(R resource) { + return String.format("'%s' %s dependent in namespace %s", resource.getMetadata().getName(), + resource.getFullResourceName(), + resource.getMetadata().getNamespace()); + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + this.client = context.getClient(); + informer = new InformerEventSource<>(configuration, context); + return informer; + } + + @Override + public void createOrReplace(R dependentResource, Context context) { + client.resource(dependentResource).createOrReplace(); + } + + @Override + public R getFor(P primary, Context context) { + return informer.getAssociated(primary).orElse(null); + } + + public boolean owned() { + return owned; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java new file mode 100644 index 0000000000..ab34372917 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface Persister { + + void createOrReplace(R dependentResource, Context context); + + R getFor(P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java new file mode 100644 index 0000000000..c92f3c5ced --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@FunctionalInterface +public interface Updater { + + R update(R fetched, P primary, Context context); +} 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 5e93b0d7af..e0b9efae77 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 @@ -1,8 +1,12 @@ package io.javaoperatorsdk.operator.processing; +import java.util.LinkedList; import java.util.List; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; @@ -25,16 +29,23 @@ 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.processing.dependent.DependentResourceManager; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +@SuppressWarnings({"rawtypes", "unchecked"}) public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { + + private static final Logger log = LoggerFactory.getLogger(Controller.class); + private final Reconciler reconciler; private final ControllerConfiguration configuration; private final KubernetesClient kubernetesClient; - private EventSourceManager eventSourceManager; - private volatile ConfigurationService configurationService; + private final EventSourceManager eventSourceManager; + private final DependentResourceManager dependents; + + private ConfigurationService configurationService; public Controller(Reconciler reconciler, ControllerConfiguration configuration, @@ -42,10 +53,15 @@ public Controller(Reconciler reconciler, this.reconciler = reconciler; this.configuration = configuration; this.kubernetesClient = kubernetesClient; + + eventSourceManager = new EventSourceManager<>(this); + dependents = new DependentResourceManager<>(this); } @Override public DeleteControl cleanup(R resource, Context context) { + dependents.cleanup(resource, context); + return metrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -72,6 +88,8 @@ public DeleteControl execute() { @Override public UpdateControl reconcile(R resource, Context context) { + dependents.reconcile(resource, context); + return metrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -103,6 +121,7 @@ public UpdateControl execute() { }); } + private Metrics metrics() { final var metrics = configurationService().getMetrics(); return metrics != null ? metrics : Metrics.NOOP; @@ -110,7 +129,14 @@ private Metrics metrics() { @Override public List prepareEventSources(EventSourceContext context) { - throw new UnsupportedOperationException("This method should never be called directly"); + final var dependentSources = dependents.prepareEventSources(context); + List sources = new LinkedList<>(dependentSources); + + // add manually defined event sources + if (reconciler instanceof EventSourceInitializer) { + sources.addAll(((EventSourceInitializer) reconciler).prepareEventSources(context)); + } + return sources; } @Override @@ -165,6 +191,8 @@ public void start() throws OperatorException { final String controllerName = configuration.getName(); final var crdName = configuration.getResourceTypeName(); final var specVersion = "v1"; + log.info("Starting '{}' controller for reconciler: {}, resource: {}", controllerName, + reconciler.getClass().getCanonicalName(), resClass.getCanonicalName()); try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config @@ -180,21 +208,23 @@ public void start() throws OperatorException { CustomResourceUtils.assertCustomResource(resClass, crd); } - eventSourceManager = new EventSourceManager<>(this); - if (reconciler instanceof EventSourceInitializer) { - ((EventSourceInitializer) reconciler) - .prepareEventSources(new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), - configurationService(), kubernetesClient)) - .forEach(eventSourceManager::registerEventSource); - } if (failOnMissingCurrentNS()) { throw new OperatorException( "Controller '" + controllerName + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } + + final var context = new EventSourceContext<>( + eventSourceManager.getControllerResourceEventSource().getResourceCache(), + configurationService(), kubernetesClient); + + dependents.injectInto(context); + prepareEventSources(context).forEach(eventSourceManager::registerEventSource); + eventSourceManager.start(); + + log.info("'{}' controller started, pending event sources initialization", controllerName); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java new file mode 100644 index 0000000000..655113d251 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -0,0 +1,140 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; +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.DependentResourceController; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; +import io.javaoperatorsdk.operator.processing.Controller; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class DependentResourceManager implements EventSourceInitializer, + EventSourceContextInjector, Reconciler { + + private static final Logger log = LoggerFactory.getLogger(DependentResourceManager.class); + + private final Reconciler reconciler; + private final ControllerConfiguration configuration; + private List dependents; + + + public DependentResourceManager(Controller controller) { + this.reconciler = controller.getReconciler(); + this.configuration = controller.getConfiguration(); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + final List configured = configuration.getDependentResources(); + dependents = new ArrayList<>(configured.size()); + + List sources = new ArrayList<>(configured.size() + 5); + configured.forEach(dependent -> { + dependents.add(configuration.dependentFactory().from(dependent)); + sources.add(dependent.initEventSource(context)); + }); + + return sources; + } + + @Override + public void injectInto(EventSourceContext context) { + if (reconciler instanceof EventSourceContextInjector) { + EventSourceContextInjector injector = (EventSourceContextInjector) reconciler; + injector.injectInto(context); + } + } + + @Override + public UpdateControl reconcile(R resource, Context context) { + initContextIfNeeded(resource, context); + + dependents.stream() + .filter(dependent -> dependent.creatable() || dependent.updatable()) + .forEach(dependent -> { + var dependentResource = dependent.getFor(resource, context); + if (dependent.creatable() && dependentResource == null) { + // we need to create the dependent + dependentResource = dependent.buildFor(resource, context); + createOrReplaceDependent(resource, context, dependent, dependentResource, "Creating"); + } else if (dependent.updatable()) { + dependentResource = dependent.update(dependentResource, resource, context); + createOrReplaceDependent(resource, context, dependent, dependentResource, "Updating"); + } else { + logOperationInfo(resource, dependent, dependentResource, "Ignoring"); + } + }); + + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(R resource, Context context) { + initContextIfNeeded(resource, context); + + dependents.stream() + .filter(DependentResourceController::deletable) + .forEach(dependent -> { + var dependentResource = dependent.getFor(resource, context); + if (dependentResource != null) { + dependent.delete(dependentResource, resource, context); + logOperationInfo(resource, dependent, dependentResource, "Deleting"); + } else { + log.info("Ignoring already deleted {} for '{}' {}", + dependent.getResourceType().getName(), + resource.getMetadata().getName(), + configuration.getResourceTypeName()); + } + }); + + return Reconciler.super.cleanup(resource, context); + } + + private void createOrReplaceDependent(R primaryResource, + Context context, DependentResourceController dependentController, + Object dependentResource, String operationDescription) { + // add owner reference if needed + if (dependentResource instanceof HasMetadata + && ((KubernetesDependentResourceController) dependentController).owned()) { + ((HasMetadata) dependentResource).addOwnerReference(primaryResource); + } + + logOperationInfo(primaryResource, dependentController, dependentResource, operationDescription); + + // commit the changes + // todo: add metrics timing for dependent resource + dependentController.createOrReplace(dependentResource, context); + } + + private void logOperationInfo(R resource, DependentResourceController dependent, + Object dependentResource, String operationDescription) { + if (log.isInfoEnabled()) { + log.info("{} {} for '{}' {}", operationDescription, + dependent.descriptionFor(dependentResource), + resource.getMetadata().getName(), + configuration.getResourceTypeName()); + } + } + + private void initContextIfNeeded(R resource, Context context) { + if (reconciler instanceof ContextInitializer) { + final var initializer = (ContextInitializer) reconciler; + initializer.initContext(resource, context); + } + } +} 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 55f42cd438..6e0b218d55 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 @@ -48,6 +48,9 @@ public EventSourceManager(Controller controller) { // controller event source needs to be available before we create the event processor final var controllerEventSource = eventSources.initControllerEventSource(controller); this.eventProcessor = new EventProcessor<>(this); + + // sources need to be registered after the event processor is created since it's set on the + // event source registerEventSource(eventSources.retryEventSource()); registerEventSource(controllerEventSource); } @@ -66,12 +69,15 @@ public EventSourceManager(Controller controller) { public void start() { lock.lock(); try { - log.debug("Starting event sources."); for (var eventSource : eventSources) { try { + logEventSourceEvent(eventSource, "Starting"); eventSource.start(); + logEventSourceEvent(eventSource, "Started"); + } catch (MissingCRDException e) { + throw e; // leave untouched } catch (Exception e) { - log.warn("Error starting {} -> {}", eventSource, e); + throw new OperatorException("Couldn't start source " + eventSource.name(), e); } } eventProcessor.start(); @@ -80,16 +86,30 @@ public void start() { } } + @SuppressWarnings("rawtypes") + private void logEventSourceEvent(EventSource eventSource, String event) { + if (log.isDebugEnabled()) { + if (eventSource instanceof ResourceEventSource) { + ResourceEventSource source = (ResourceEventSource) eventSource; + log.debug("{} event source {} for {}", event, eventSource.name(), + source.getResourceClass()); + } else { + log.debug("{} event source {}", event, eventSource.name()); + } + } + } + @Override public void stop() { lock.lock(); try { - log.debug("Closing event sources."); for (var eventSource : eventSources) { try { + logEventSourceEvent(eventSource, "Stopping"); eventSource.stop(); + logEventSourceEvent(eventSource, "Stopped"); } catch (Exception e) { - log.warn("Error closing {} -> {}", eventSource, e); + log.warn("Error closing {} -> {}", eventSource.name(), e); } } eventSources.clear(); @@ -106,13 +126,10 @@ public final void registerEventSource(EventSource eventSource) try { eventSources.add(eventSource); eventSource.setEventHandler(eventProcessor); - } catch (Throwable e) { - if (e instanceof IllegalStateException || e instanceof MissingCRDException) { - // leave untouched - throw e; - } - throw new OperatorException( - "Couldn't register event source: " + eventSource.getClass().getName(), e); + } catch (IllegalStateException | MissingCRDException e) { + throw e; // leave untouched + } catch (Exception e) { + throw new OperatorException("Couldn't register event source: " + eventSource.name(), e); } finally { lock.unlock(); } @@ -219,7 +236,7 @@ public void add(EventSource eventSource) { sources.computeIfAbsent(keyFor(eventSource), k -> new ArrayList<>()).add(eventSource); } - private Class getDependentType(EventSource source) { + private Class getDependentType(EventSource source) { return source instanceof ResourceEventSource ? ((ResourceEventSource) source).getResourceClass() : source.getClass(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java index 5fa45e0a25..6767e974e2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AbstractEventSource.java @@ -5,7 +5,7 @@ public abstract class AbstractEventSource implements EventSource { - private volatile EventHandler handler; + private EventHandler handler; private volatile boolean running = false; protected EventHandler getEventHandler() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index 486ffb81a6..aac04115a3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -7,7 +7,6 @@ import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -28,7 +27,7 @@ public abstract class CachingEventSource protected UpdatableCache cache; - public CachingEventSource(Class resourceClass) { + protected CachingEventSource(Class resourceClass) { super(resourceClass); cache = initCache(); } @@ -84,11 +83,6 @@ public Optional getCachedValue(ResourceID resourceID) { return cache.get(resourceID); } - @Override - public void stop() throws OperatorException { - super.stop(); - } - @Override public Optional getAssociated(P primary) { return cache.get(ResourceID.fromResource(primary)); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java new file mode 100644 index 0000000000..95042d0c41 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; + +public interface EventSourceContextAware

{ + void initWith(EventSourceContext

context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 9feabf40bf..2a224a87df 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -1,34 +1,27 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; -import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.javaoperatorsdk.operator.MissingCRDException; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; public class ControllerResourceEventSource - extends AbstractResourceEventSource + extends ManagedInformerEventSource> implements ResourceEventHandler { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; @@ -36,20 +29,12 @@ public class ControllerResourceEventSource private static final Logger log = LoggerFactory.getLogger(ControllerResourceEventSource.class); private final Controller controller; - private final Map> sharedIndexInformers = - new ConcurrentHashMap<>(); - private final ResourceEventFilter filter; private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; - private final ControllerResourceCache cache; public ControllerResourceEventSource(Controller controller) { - super(controller.getConfiguration().getResourceClass()); + super(controller.getCRClient(), controller.getConfiguration()); this.controller = controller; - final var configurationService = controller.getConfiguration().getConfigurationService(); - var cloner = configurationService != null ? configurationService.getResourceCloner() - : ConfigurationService.DEFAULT_CLONER; - this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), @@ -73,70 +58,27 @@ public ControllerResourceEventSource(Controller controller) { @Override public void start() { - final var configuration = controller.getConfiguration(); - final var targetNamespaces = configuration.getEffectiveNamespaces(); - final var client = controller.getCRClient(); - final var labelSelector = configuration.getLabelSelector(); - try { - if (ControllerConfiguration.allNamespacesWatched(targetNamespaces)) { - final var informer = - createAndRunInformerFor(client.inAnyNamespace() - .withLabelSelector(labelSelector), ANY_NAMESPACE_MAP_KEY); - log.debug("Registered {} -> {} for any namespace", controller, informer); - } else { - targetNamespaces.forEach(ns -> { - final var informer = createAndRunInformerFor( - client.inNamespace(ns).withLabelSelector(labelSelector), ns); - log.debug("Registered {} -> {} for namespace: {}", controller, informer, ns); - }); - } + super.start(); } catch (Exception e) { if (e instanceof KubernetesClientException) { handleKubernetesClientException(e); } throw e; } - super.start(); - } - - private SharedIndexInformer createAndRunInformerFor( - FilterWatchListDeletable> filteredBySelectorClient, String key) { - var informer = filteredBySelectorClient.runnableInformer(0); - informer.addEventHandler(this); - sharedIndexInformers.put(key, informer); - informer.run(); - return informer; } - @Override - public void stop() { - for (SharedIndexInformer informer : sharedIndexInformers.values()) { - try { - log.info("Stopping informer {} -> {}", controller, informer); - informer.stop(); - } catch (Exception e) { - log.warn("Error stopping informer {} -> {}", controller, informer, e); - } - } - super.stop(); - } - - public void eventReceived(ResourceAction action, T customResource, T oldResource) { + public void eventReceived(ResourceAction action, T resource, T oldResource) { try { - log.debug( - "Event received for resource: {}", getName(customResource)); - MDCUtils.addResourceInfo(customResource); - controller.getEventSourceManager().broadcastOnResourceEvent(action, customResource, - oldResource); - if (filter.acceptChange(controller.getConfiguration(), oldResource, customResource)) { + log.debug("Event received for resource: {}", getName(resource)); + MDCUtils.addResourceInfo(resource); + controller.getEventSourceManager().broadcastOnResourceEvent(action, resource, oldResource); + if (filter.acceptChange(controller.getConfiguration(), oldResource, resource)) { getEventHandler().handleEvent( - new ResourceEvent(action, ResourceID.fromResource(customResource))); + new ResourceEvent(action, ResourceID.fromResource(resource))); } else { - log.debug( - "Skipping event handling resource {} with version: {}", - getUID(customResource), - getVersion(customResource)); + log.debug("Skipping event handling resource {} with version: {}", getUID(resource), + getVersion(resource)); } } finally { MDCUtils.removeResourceInfo(); @@ -158,24 +100,8 @@ public void onDelete(T resource, boolean b) { eventReceived(ResourceAction.DELETED, resource, null); } - public Optional get(ResourceID resourceID) { - return cache.get(resourceID); - } - - public ControllerResourceCache getResourceCache() { - return cache; - } - - /** - * @return shared informers by namespace. If custom resource is not namespace scoped use - * CustomResourceEventSource.ANY_NAMESPACE_MAP_KEY - */ - public Map> getInformers() { - return Collections.unmodifiableMap(sharedIndexInformers); - } - - public SharedIndexInformer getInformer(String namespace) { - return getInformers().get(Objects.requireNonNullElse(namespace, ANY_NAMESPACE_MAP_KEY)); + public ResourceCache getResourceCache() { + return manager(); } /** @@ -204,6 +130,6 @@ private void handleKubernetesClientException(Exception e) { @Override public Optional getAssociated(T primary) { - return cache.get(ResourceID.fromResource(primary)); + return manager().get(ResourceID.fromResource(primary)); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java new file mode 100644 index 0000000000..10bfdcdcb8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java @@ -0,0 +1,132 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; + +public class InformerConfiguration + extends DefaultResourceConfiguration { + + private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; + private final AssociatedSecondaryResourceIdentifier

associatedWith; + private final boolean skipUpdateEventPropagationIfNoChange; + + private InformerConfiguration(ConfigurationService service, String labelSelector, + Class resourceClass, + PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange, Set namespaces) { + super(labelSelector, resourceClass, namespaces); + setConfigurationService(service); + this.secondaryToPrimaryResourcesIdSet = + Objects.requireNonNullElse(secondaryToPrimaryResourcesIdSet, Mappers.fromOwnerReference()); + this.associatedWith = + Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); + this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; + } + + public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { + return secondaryToPrimaryResourcesIdSet; + } + + public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier() { + return associatedWith; + } + + public boolean isSkipUpdateEventPropagationIfNoChange() { + return skipUpdateEventPropagationIfNoChange; + } + + public static class InformerConfigurationBuilder { + + private PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; + private AssociatedSecondaryResourceIdentifier

associatedWith; + private boolean skipUpdateEventPropagationIfNoChange = true; + private Set namespaces; + private String labelSelector; + private final Class resourceClass; + private final ConfigurationService configurationService; + + private InformerConfigurationBuilder(Class resourceClass, + ConfigurationService configurationService) { + this.resourceClass = resourceClass; + this.configurationService = configurationService; + } + + public InformerConfigurationBuilder withPrimaryResourcesRetriever( + PrimaryResourcesRetriever primaryResourcesRetriever) { + this.secondaryToPrimaryResourcesIdSet = primaryResourcesRetriever; + return this; + } + + public InformerConfigurationBuilder withAssociatedSecondaryResourceIdentifier( + AssociatedSecondaryResourceIdentifier

associatedWith) { + this.associatedWith = associatedWith; + return this; + } + + public InformerConfigurationBuilder withoutSkippingEventPropagationIfUnchanged() { + this.skipUpdateEventPropagationIfNoChange = false; + return this; + } + + public InformerConfigurationBuilder skippingEventPropagationIfUnchanged( + boolean skipIfUnchanged) { + this.skipUpdateEventPropagationIfNoChange = skipIfUnchanged; + return this; + } + + public InformerConfigurationBuilder withNamespaces(String... namespaces) { + this.namespaces = namespaces != null ? Set.of(namespaces) : Collections.emptySet(); + return this; + } + + public InformerConfigurationBuilder withNamespaces(Set namespaces) { + this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); + return this; + } + + + public InformerConfigurationBuilder withLabelSelector(String labelSelector) { + this.labelSelector = labelSelector; + return this; + } + + public InformerConfiguration build() { + return new InformerConfiguration<>(configurationService, labelSelector, resourceClass, + secondaryToPrimaryResourcesIdSet, associatedWith, skipUpdateEventPropagationIfNoChange, + namespaces); + } + } + + public static InformerConfigurationBuilder from( + EventSourceContext

context, Class resourceClass) { + return new InformerConfigurationBuilder<>(resourceClass, context.getConfigurationService()); + } + + public static InformerConfigurationBuilder from(ConfigurationService configurationService, + Class resourceClass) { + return new InformerConfigurationBuilder<>(resourceClass, configurationService); + } + + public static InformerConfigurationBuilder from( + InformerConfiguration configuration) { + return new InformerConfigurationBuilder(configuration.getResourceClass(), + configuration.getConfigurationService()) + .withNamespaces(configuration.getNamespaces()) + .withLabelSelector(configuration.getLabelSelector()) + .skippingEventPropagationIfUnchanged( + configuration.isSkipUpdateEventPropagationIfNoChange()) + .withAssociatedSecondaryResourceIdentifier( + configuration.getAssociatedResourceIdentifier()) + .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index eada096713..fa9e516585 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -1,112 +1,70 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; -import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; -import io.fabric8.kubernetes.client.informers.SharedInformer; -import io.fabric8.kubernetes.client.informers.cache.Store; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; public class InformerEventSource - extends AbstractResourceEventSource - implements ResourceCache { - - private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); + extends ManagedInformerEventSource> + implements ResourceCache, ResourceEventHandler { - private final SharedInformer sharedInformer; - private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; - private final AssociatedSecondaryResourceIdentifier

associatedWith; - private final boolean skipUpdateEventPropagationIfNoChange; + private final InformerConfiguration configuration; - public InformerEventSource(SharedInformer sharedInformer, - PrimaryResourcesRetriever resourceToTargetResourceIDSet) { - this(sharedInformer, resourceToTargetResourceIDSet, null, true); - } + public InformerEventSource(InformerConfiguration configuration, + EventSourceContext

context) { + super(context.getClient().resources(configuration.getResourceClass()), configuration); + this.configuration = configuration; - public InformerEventSource(KubernetesClient client, Class type, - PrimaryResourcesRetriever resourceToTargetResourceIDSet) { - this(client, type, resourceToTargetResourceIDSet, false); - } + // init mappers with context if needed + final var primaryResourcesRetriever = configuration.getPrimaryResourcesRetriever(); + if (primaryResourcesRetriever instanceof EventSourceContextAware) { + ((EventSourceContextAware) primaryResourcesRetriever).initWith(context); + } - public InformerEventSource(KubernetesClient client, Class type, - PrimaryResourcesRetriever resourceToTargetResourceIDSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, - associatedWith, - skipUpdateEventPropagationIfNoChange); + final var associatedResourceIdentifier = configuration.getAssociatedResourceIdentifier(); + if (associatedResourceIdentifier instanceof EventSourceContextAware) { + ((EventSourceContextAware) associatedResourceIdentifier).initWith(context); + } } - InformerEventSource(KubernetesClient client, Class type, - PrimaryResourcesRetriever resourceToTargetResourceIDSet, - boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, null, - skipUpdateEventPropagationIfNoChange); + @Override + public void onAdd(T t) { + propagateEvent(t); } - public InformerEventSource(SharedInformer sharedInformer, - PrimaryResourcesRetriever resourceToTargetResourceIDSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange) { - super(sharedInformer.getApiTypeClass()); - this.sharedInformer = sharedInformer; - this.secondaryToPrimaryResourcesIdSet = resourceToTargetResourceIDSet; - this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; - if (sharedInformer.isRunning()) { - log.warn( - "Informer is already running on event source creation, this is not desirable and may " + - "lead to non deterministic behavior."); + @Override + public void onUpdate(T oldObject, T newObject) { + if (newObject == null) { + // this is a fix for this potential issue with informer: + // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 + propagateEvent(oldObject); + return; } - this.associatedWith = - Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); - - sharedInformer.addEventHandler(new ResourceEventHandler<>() { - @Override - public void onAdd(T t) { - propagateEvent(t); - } - - @Override - public void onUpdate(T oldObject, T newObject) { - if (newObject == null) { - // this is a fix for this potential issue with informer: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 - propagateEvent(oldObject); - return; - } - - if (InformerEventSource.this.skipUpdateEventPropagationIfNoChange && - oldObject.getMetadata().getResourceVersion() - .equals(newObject.getMetadata().getResourceVersion())) { - return; - } - propagateEvent(newObject); - } + if (configuration.isSkipUpdateEventPropagationIfNoChange() && + oldObject.getMetadata().getResourceVersion() + .equals(newObject.getMetadata().getResourceVersion())) { + return; + } + propagateEvent(newObject); + } - @Override - public void onDelete(T t, boolean b) { - propagateEvent(t); - } - }); + @Override + public void onDelete(T t, boolean b) { + propagateEvent(t); } private void propagateEvent(T object) { - var primaryResourceIdSet = secondaryToPrimaryResourcesIdSet.associatedPrimaryResources(object); + var primaryResourceIdSet = + configuration.getPrimaryResourcesRetriever().associatedPrimaryResources(object); if (primaryResourceIdSet.isEmpty()) { return; } @@ -126,16 +84,12 @@ private void propagateEvent(T object) { @Override public void start() { - sharedInformer.run(); + manager().start(); } @Override public void stop() { - sharedInformer.close(); - } - - private Store getStore() { - return sharedInformer.getStore(); + manager().stop(); } /** @@ -145,37 +99,14 @@ private Store getStore() { * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ + @Override public Optional getAssociated(P resource) { - final var id = associatedWith.associatedSecondaryID(resource); + final var id = configuration.getAssociatedResourceIdentifier().associatedSecondaryID(resource); return get(id); } - - public SharedInformer getSharedInformer() { - return sharedInformer; - } - - @Override - public Optional get(ResourceID resourceID) { - return Optional.ofNullable(sharedInformer.getStore() - .getByKey(io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( - resourceID.getNamespace().orElse(null), - resourceID.getName()))); - } - - @Override - public Stream list(Predicate predicate) { - return getStore().list().stream().filter(predicate); - } - @Override public Stream list(String namespace, Predicate predicate) { - return getStore().list().stream() - .filter(v -> namespace.equals(v.getMetadata().getNamespace()) && predicate.test(v)); - } - - @Override - public Stream keys() { - return getStore().listKeys().stream().map(Mappers::fromString); + return manager().list(namespace, predicate); } } 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 new file mode 100644 index 0000000000..e65f34df48 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -0,0 +1,144 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.Cloner; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +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.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +public class InformerManager> + implements LifecycleAware, ResourceCache, UpdatableCache { + + private static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; + private static final Logger log = LoggerFactory.getLogger(InformerManager.class); + + private final Map> sources = new ConcurrentHashMap<>(); + private Cloner cloner; + + @Override + public void start() throws OperatorException { + sources.values().parallelStream().forEach(LifecycleAware::start); + } + + void initSources(MixedOperation, Resource> client, + C configuration, ResourceEventHandler eventHandler) { + final var service = configuration.getConfigurationService(); + cloner = service == null ? ConfigurationService.DEFAULT_CLONER : service.getResourceCloner(); + + final var targetNamespaces = configuration.getEffectiveNamespaces(); + final var labelSelector = configuration.getLabelSelector(); + + if (ResourceConfiguration.allNamespacesWatched(targetNamespaces)) { + final var filteredBySelectorClient = + client.inAnyNamespace().withLabelSelector(labelSelector); + final var source = + createEventSource(filteredBySelectorClient, eventHandler, ANY_NAMESPACE_MAP_KEY); + log.debug("Registered {} -> {} for any namespace", this, source); + } else { + targetNamespaces.forEach( + ns -> { + final var source = + createEventSource(client.inNamespace(ns).withLabelSelector(labelSelector), + eventHandler, ns); + log.debug("Registered {} -> {} for namespace: {}", this, source, + ns); + }); + } + } + + + private InformerWrapper createEventSource( + FilterWatchListDeletable> filteredBySelectorClient, + ResourceEventHandler eventHandler, String key) { + var source = new InformerWrapper<>(filteredBySelectorClient.runnableInformer(0)); + source.addEventHandler(eventHandler); + sources.put(key, source); + return source; + } + + @Override + public void stop() { + for (InformerWrapper source : sources.values()) { + try { + log.info("Stopping informer {} -> {}", this, source); + source.stop(); + } catch (Exception e) { + log.warn("Error stopping informer {} -> {}", this, source, e); + } + } + } + + @Override + public Stream list(Predicate predicate) { + if (predicate == null) { + return sources.values().stream().flatMap(ResourceCache::list); + } + return sources.values().stream().flatMap(i -> i.list(predicate)); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + if (isWatchingAllNamespaces()) { + return getSource(ANY_NAMESPACE_MAP_KEY) + .map(source -> source.list(namespace, predicate)) + .orElse(Stream.empty()); + } else { + return getSource(namespace) + .map(source -> source.list(predicate)) + .orElse(Stream.empty()); + } + } + + @Override + public Optional get(ResourceID resourceID) { + return getSource(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) + .flatMap(source -> source.get(resourceID)) + .map(cloner::clone); + } + + @Override + public Stream keys() { + return sources.values().stream().flatMap(Cache::keys); + } + + private boolean isWatchingAllNamespaces() { + return sources.containsKey(ANY_NAMESPACE_MAP_KEY); + } + + private Optional> getSource(String namespace) { + namespace = isWatchingAllNamespaces() || namespace == null ? ANY_NAMESPACE_MAP_KEY : namespace; + return Optional.ofNullable(sources.get(namespace)); + } + + @Override + public T remove(ResourceID key) { + return getSource(key.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) + .map(c -> c.remove(key)) + .orElse(null); + } + + @Override + public void put(ResourceID key, T resource) { + getSource(key.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) + .ifPresent(c -> c.put(key, resource)); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerResourceCache.java new file mode 100644 index 0000000000..5199b4fb0d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerResourceCache.java @@ -0,0 +1,64 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.SharedInformer; +import io.fabric8.kubernetes.client.informers.cache.Cache; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +class InformerResourceCache implements ResourceCache, UpdatableCache { + + private final Cache cache; + + public InformerResourceCache(SharedInformer informer) { + this.cache = (Cache) informer.getStore(); + } + + @Override + public Optional get(ResourceID resourceID) { + return Optional.ofNullable(cache.getByKey(getKey(resourceID))); + } + + private String getKey(ResourceID resourceID) { + return Cache.namespaceKeyFunc(resourceID.getNamespace().orElse(null), resourceID.getName()); + } + + @Override + public Stream list(Predicate predicate) { + return cache.list().stream().filter(predicate); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + final var stream = cache.list().stream() + .filter(r -> namespace.equals(r.getMetadata().getNamespace())); + return predicate != null ? stream.filter(predicate) : stream; + } + + @Override + public Stream keys() { + return cache.listKeys().stream().map(Mappers::fromString); + } + + @Override + public T remove(ResourceID key) { + return cache.remove(cache.getByKey(getKey(key))); + } + + @Override + public void put(ResourceID key, T resource) { + // check that key matches the resource + final var fromResource = ResourceID.fromResource(resource); + if (!Objects.equals(key, fromResource)) { + throw new IllegalArgumentException( + "Key and resource don't match. Key: " + key + ", resource: " + fromResource); + } + cache.put(resource); + } +} 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 new file mode 100644 index 0000000000..c7de41f331 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -0,0 +1,84 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.LifecycleAware; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +class InformerWrapper + implements LifecycleAware, ResourceCache, UpdatableCache { + private final SharedIndexInformer informer; + private final InformerResourceCache cache; + + public InformerWrapper(SharedIndexInformer informer) { + this.informer = informer; + this.cache = new InformerResourceCache<>(informer); + } + + @Override + public void start() throws OperatorException { + informer.run(); + } + + @Override + public void stop() throws OperatorException { + informer.stop(); + } + + @Override + public Optional get(ResourceID resourceID) { + return cache.get(resourceID); + } + + @Override + public boolean contains(ResourceID resourceID) { + return cache.contains(resourceID); + } + + @Override + public Stream keys() { + return cache.keys(); + } + + @Override + public Stream list() { + return cache.list(); + } + + @Override + public Stream list(Predicate predicate) { + return cache.list(predicate); + } + + @Override + public Stream list(String namespace) { + return cache.list(namespace); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + return cache.list(namespace, predicate); + } + + public void addEventHandler(ResourceEventHandler eventHandler) { + informer.addEventHandler(eventHandler); + } + + @Override + public T remove(ResourceID key) { + return cache.remove(key); + } + + @Override + public void put(ResourceID key, T resource) { + cache.put(key, resource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java new file mode 100644 index 0000000000..f0a1a1185d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -0,0 +1,43 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +public abstract class ManagedInformerEventSource> + extends CachingEventSource + implements ResourceEventHandler { + + protected ManagedInformerEventSource( + MixedOperation, Resource> client, C configuration) { + super(configuration.getResourceClass()); + manager().initSources(client, configuration, this); + } + + @Override + protected UpdatableCache initCache() { + return new InformerManager<>(); + } + + protected InformerManager manager() { + return (InformerManager) cache; + } + + @Override + public void start() { + manager().start(); + super.start(); + } + + + @Override + public void stop() { + super.stop(); + manager().stop(); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index 053528e0ff..21cd3b162a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -9,6 +9,8 @@ public class Mappers { + private Mappers() {} + public static PrimaryResourcesRetriever fromAnnotation( String nameKey) { return fromMetadata(nameKey, null, false); 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 a1cfa3e82d..73d3382cc4 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 @@ -44,8 +44,10 @@ private void checkException( TestControllerConfiguration duplicated) { final var exception = assertThrows(OperatorException.class, () -> { final var controllerManager = new ControllerManager(); - controllerManager.add(new Controller<>(registered.controller, registered, null)); - controllerManager.add(new Controller<>(duplicated.controller, duplicated, null)); + controllerManager.add(new Controller<>(registered.controller, registered, + MockKubernetesClient.client(registered.getResourceClass()))); + controllerManager.add(new Controller<>(duplicated.controller, duplicated, + MockKubernetesClient.client(duplicated.getResourceClass()))); }); final var msg = exception.getMessage(); assertTrue( @@ -60,7 +62,8 @@ private static class TestControllerConfiguration public TestControllerConfiguration(Reconciler controller, Class crClass) { super(null, getControllerName(controller), - CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null); + CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null, + null); this.controller = controller; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java new file mode 100644 index 0000000000..0c2db0f6b1 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/MockKubernetesClient.java @@ -0,0 +1,52 @@ +package io.javaoperatorsdk.operator; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.V1ApiextensionAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.ApiextensionsAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockKubernetesClient { + + public static KubernetesClient client(Class clazz) { + final var client = mock(KubernetesClient.class); + MixedOperation, Resource> resources = + mock(MixedOperation.class); + NonNamespaceOperation, Resource> nonNamespaceOperation = + mock(NonNamespaceOperation.class); + FilterWatchListMultiDeletable> inAnyNamespace = mock( + FilterWatchListMultiDeletable.class); + FilterWatchListDeletable> filterable = + mock(FilterWatchListDeletable.class); + when(resources.inNamespace(anyString())).thenReturn(nonNamespaceOperation); + when(nonNamespaceOperation.withLabelSelector(nullable(String.class))).thenReturn(filterable); + when(resources.inAnyNamespace()).thenReturn(inAnyNamespace); + when(inAnyNamespace.withLabelSelector(nullable(String.class))).thenReturn(filterable); + SharedIndexInformer informer = mock(SharedIndexInformer.class); + when(filterable.runnableInformer(anyLong())).thenReturn(informer); + when(client.resources(clazz)).thenReturn(resources); + + final var apiGroupDSL = mock(ApiextensionsAPIGroupDSL.class); + when(client.apiextensions()).thenReturn(apiGroupDSL); + final var v1 = mock(V1ApiextensionAPIGroupDSL.class); + when(apiGroupDSL.v1()).thenReturn(v1); + final var operation = mock(NonNamespaceOperation.class); + when(v1.customResourceDefinitions()).thenReturn(operation); + when(operation.withName(any())).thenReturn(mock(Resource.class)); + + return client; + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index c234a0b9b2..f532756f58 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -19,7 +19,8 @@ class OperatorTest { - private final KubernetesClient kubernetesClient = mock(KubernetesClient.class); + private final KubernetesClient kubernetesClient = + MockKubernetesClient.client(FooCustomResource.class); private final ConfigurationService configurationService = mock(ConfigurationService.class); private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); private final Operator operator = new Operator(kubernetesClient, configurationService); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java index 3bc72d81f2..65c73b6b31 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -20,6 +20,9 @@ public String getAssociatedReconcilerClassName() { public ConfigurationService getConfigurationService() { return null; } + + @Override + public void setConfigurationService(ConfigurationService service) {} }; assertEquals(TestCustomResource.class, conf.getResourceClass()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index d61b50d583..2954af5aa4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -3,19 +3,14 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.V1ApiextensionAPIGroupDSL; -import io.fabric8.kubernetes.client.dsl.ApiextensionsAPIGroupDSL; -import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; -import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -26,7 +21,7 @@ class ControllerTest { @Test void crdShouldNotBeCheckedForNativeResources() { - final var client = mock(KubernetesClient.class); + final var client = MockKubernetesClient.client(Secret.class); final var configurationService = mock(ConfigurationService.class); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); @@ -40,7 +35,7 @@ void crdShouldNotBeCheckedForNativeResources() { @Test void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { - final var client = mock(KubernetesClient.class); + final var client = MockKubernetesClient.client(TestCustomResource.class); final var configurationService = mock(ConfigurationService.class); when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); final var reconciler = mock(Reconciler.class); @@ -55,20 +50,13 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { @Test void crdShouldBeCheckedForCustomResourcesByDefault() { - final var client = mock(KubernetesClient.class); + final var client = MockKubernetesClient.client(TestCustomResource.class); final var configurationService = mock(ConfigurationService.class); when(configurationService.checkCRDAndValidateLocalModel()).thenCallRealMethod(); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); when(configuration.getConfigurationService()).thenReturn(configurationService); - final var apiGroupDSL = mock(ApiextensionsAPIGroupDSL.class); - when(client.apiextensions()).thenReturn(apiGroupDSL); - final var v1 = mock(V1ApiextensionAPIGroupDSL.class); - when(apiGroupDSL.v1()).thenReturn(v1); - final var operation = mock(NonNamespaceOperation.class); - when(v1.customResourceDefinitions()).thenReturn(operation); - when(operation.withName(any())).thenReturn(mock(Resource.class)); final var controller = new Controller(reconciler, configuration, client); // since we're not really connected to a cluster and the CRD wouldn't be deployed anyway, we diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java index a330b4130c..64e742d381 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventSourceManagerTest.java @@ -8,8 +8,10 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -169,10 +171,10 @@ void timerAndControllerEventSourcesShouldBeListedFirst() { } private EventSourceManager initManager() { - final Controller controller = mock(Controller.class); final ControllerConfiguration configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(HasMetadata.class); - when(controller.getConfiguration()).thenReturn(configuration); + final Controller controller = new Controller(mock(Reconciler.class), configuration, + MockKubernetesClient.client(HasMetadata.class)); return new EventSourceManager(controller); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 9da51337d3..9cd7d9481b 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 @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.CustomResource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.api.config.ConfigurationService; @@ -90,7 +91,9 @@ public T clone(T object) { }); when(reconciler.cleanup(eq(customResource), any())) .thenReturn(DeleteControl.defaultDelete()); - Controller controller = new Controller<>(reconciler, configuration, null); + Controller controller = new Controller<>(reconciler, configuration, + MockKubernetesClient.client(customResource.getClass())); + controller.start(); return new ReconciliationDispatcher<>(controller, customResourceFacade); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index ce865b7888..446e654501 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -158,6 +158,9 @@ public Set getNamespaces() { public ConfigurationService getConfigurationService() { return service; } + + @Override + public void setConfigurationService(ConfigurationService service) {} } @ControllerConfiguration(namespaces = NAMESPACE) 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 17168876a5..6c5e306263 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 @@ -7,10 +7,8 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; @@ -171,6 +169,7 @@ public ControllerConfig(String finalizer, boolean generationAware, null, eventFilter, customResourceClass, + null, null); } } @@ -178,18 +177,13 @@ public ControllerConfig(String finalizer, boolean generationAware, private static class TestController extends Controller { public TestController(ControllerConfiguration configuration) { - super(null, configuration, null); + super(null, configuration, MockKubernetesClient.client(TestCustomResource.class)); } @Override public EventSourceManager getEventSourceManager() { return mock(EventSourceManager.class); } - - @Override - public MixedOperation, Resource> getCRClient() { - return mock(MixedOperation.class); - } } private static class ObservedGenController @@ -197,17 +191,12 @@ private static class ObservedGenController public ObservedGenController( ControllerConfiguration configuration) { - super(null, configuration, null); + super(null, configuration, MockKubernetesClient.client(ObservedGenCustomResource.class)); } @Override public EventSourceManager getEventSourceManager() { return mock(EventSourceManager.class); } - - @Override - public MixedOperation, Resource> getCRClient() { - return mock(MixedOperation.class); - } } } 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 5cdc85c553..f67ea44a5d 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 @@ -6,9 +6,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.processing.Controller; @@ -28,8 +26,6 @@ class ControllerResourceEventSourceTest extends AbstractEventSourceTestBase, EventHandler> { public static final String FINALIZER = "finalizer"; - private static final MixedOperation, Resource> client = - mock(MixedOperation.class); private TestController testController = new TestController(true); @@ -141,18 +137,14 @@ private static class TestController extends Controller { mock(EventSourceManager.class); public TestController(boolean generationAware) { - super(null, new TestConfiguration(generationAware), null); + super(null, new TestConfiguration(generationAware), + MockKubernetesClient.client(TestCustomResource.class)); } @Override public EventSourceManager getEventSourceManager() { return eventSourceManager; } - - @Override - public MixedOperation, Resource> getCRClient() { - return client; - } } private static class TestConfiguration extends @@ -170,6 +162,7 @@ public TestConfiguration(boolean generationAware) { null, null, TestCustomResource.class, + null, null); } } diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 69a163b7d7..6f66b97810 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index fd124ea222..8e7ff3fe8f 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 1000cb61c4..89a100741f 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,22 +1,33 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; 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.informer.InformerConfiguration; +@SuppressWarnings("rawtypes") public class AnnotationConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; + private List dependents; public AnnotationConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -108,14 +119,54 @@ public ResourceEventFilter getEventFilter() { : ResourceEventFilters.passthrough(); } - public static T valueOrDefault(ControllerConfiguration controllerConfiguration, - Function mapper, - T defaultValue) { - if (controllerConfiguration == null) { - return defaultValue; - } else { - return mapper.apply(controllerConfiguration); + @Override + public List getDependentResources() { + if (dependents == null) { + final var dependentConfigs = valueOrDefault(annotation, + ControllerConfiguration::dependents, new Dependent[] {}); + if (dependentConfigs.length > 0) { + dependents = new ArrayList<>(dependentConfigs.length); + for (Dependent dependentConfig : dependentConfigs) { + final Class dependentType = dependentConfig.type(); + DependentResource dependent; + try { + dependent = dependentType.getConstructor().newInstance(); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException + | IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + + final var resourceType = dependentConfig.resourceType(); + if (HasMetadata.class.isAssignableFrom(resourceType)) { + final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); + final var namespaces = + valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[] {}); + final var labelSelector = + valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); + final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, + KubernetesDependent.OWNED_DEFAULT); + final var skipIfUnchanged = + valueOrDefault(kubeDependent, KubernetesDependent::skipUpdateIfUnchanged, + KubernetesDependent.SKIP_UPDATE_DEFAULT); + final var configuration = InformerConfiguration.from(service, resourceType) + .withLabelSelector(labelSelector) + .skippingEventPropagationIfUnchanged(skipIfUnchanged) + .withNamespaces(namespaces) + .build(); + dependent = new KubernetesDependentResourceController(dependent, configuration, owned); + } + + dependents.add(dependent); + } + } else { + dependents = Collections.emptyList(); + } } + return dependents; + } + + private static T valueOrDefault(C annotation, Function mapper, T defaultValue) { + return annotation == null ? defaultValue : mapper.apply(annotation); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index b7ceb4c11c..2a04b85d19 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -1,24 +1,23 @@ package io.javaoperatorsdk.operator.sample.informereventsource; -import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.config.DependentResource; 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.junit.KubernetesClientAware; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; +import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.ConfigMapDR; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -26,10 +25,10 @@ * Copies the config map value from spec into status. The main purpose is to test and demonstrate * sample usage of InformerEventSource */ -@ControllerConfiguration(finalizerName = NO_FINALIZER) -public class InformerEventSourceTestCustomReconciler implements - Reconciler, KubernetesClientAware, - EventSourceInitializer { +@ControllerConfiguration(finalizerName = NO_FINALIZER, + dependents = @Dependent(resourceType = ConfigMap.class, type = ConfigMapDR.class)) +public class InformerEventSourceTestCustomReconciler + implements Reconciler { private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomReconciler.class); @@ -38,14 +37,18 @@ public class InformerEventSourceTestCustomReconciler implements public static final String TARGET_CONFIG_MAP_KEY = "targetStatus"; public static final String MISSING_CONFIG_MAP = "Missing Config Map"; - private KubernetesClient kubernetesClient; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); - @Override - public List prepareEventSources( - EventSourceContext context) { - return List.of(new InformerEventSource<>(kubernetesClient, ConfigMap.class, - Mappers.fromAnnotation(RELATED_RESOURCE_NAME))); + public static class ConfigMapDR + implements DependentResource, + PrimaryResourcesRetriever { + private final PrimaryResourcesRetriever retriever = Mappers.fromAnnotation( + RELATED_RESOURCE_NAME); + + @Override + public Set associatedPrimaryResources(ConfigMap dependentResource) { + return retriever.associatedPrimaryResources(dependentResource); + } } @Override @@ -69,16 +72,6 @@ public UpdateControl reconcile( return UpdateControl.updateStatus(resource); } - @Override - public KubernetesClient getKubernetesClient() { - return kubernetesClient; - } - - @Override - public void setKubernetesClient(KubernetesClient kubernetesClient) { - this.kubernetesClient = kubernetesClient; - } - public int getNumberOfExecutions() { return numberOfExecutions.get(); } diff --git a/pom.xml b/pom.xml index a520155deb..fbca5b3ae2 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 58d1f3155a..64be615e03 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.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 ebccbd4fe5..d9012637cf 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 @@ -32,11 +32,10 @@ public static void main(String[] args) throws IOException { new ConfigurationServiceOverrider(DefaultConfigurationService.instance()) .withMetrics(new MicrometerMetrics(new LoggingMeterRegistry())) .build()); - operator.register(new MySQLSchemaReconciler(client, MySQLDbConfig.loadFromEnvironmentVars())); + operator.register(new MySQLSchemaReconciler(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/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index af77b6ce97..ecc2aa9be7 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -1,101 +1,107 @@ package io.javaoperatorsdk.operator.sample; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; import java.util.Base64; -import java.util.List; import java.util.Optional; import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.SecretDependentResource; import io.javaoperatorsdk.operator.sample.schema.Schema; -import io.javaoperatorsdk.operator.sample.schema.SchemaService; import static java.lang.String.format; -@ControllerConfiguration +@ControllerConfiguration( + dependents = { + @Dependent(resourceType = Secret.class, type = SecretDependentResource.class), + @Dependent(resourceType = Schema.class, type = SchemaDependentResource.class) + }) public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler, - EventSourceInitializer { - public static final String SECRET_FORMAT = "%s-secret"; - public static final String USERNAME_FORMAT = "%s-user"; - public static final int POLL_PERIOD = 500; - private final Logger log = LoggerFactory.getLogger(getClass()); + ContextInitializer, EventSourceContextInjector { + + private static final String SECRET_FORMAT = "%s-secret"; + private static final String USERNAME_FORMAT = "%s-user"; + + protected static final String MYSQL_SECRET_NAME = "mysql.secret.name"; + protected static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; + protected static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; + protected static final String MYSQL_DB_CONFIG = "mysql.db.config"; + protected static final String BUILT_SCHEMA = "built schema"; + static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class); - private final KubernetesClient kubernetesClient; private final MySQLDbConfig mysqlDbConfig; - public MySQLSchemaReconciler(KubernetesClient kubernetesClient, MySQLDbConfig mysqlDbConfig) { - this.kubernetesClient = kubernetesClient; + public MySQLSchemaReconciler(MySQLDbConfig mysqlDbConfig) { this.mysqlDbConfig = mysqlDbConfig; } + public static class SecretDependentResource + implements DependentResource, Builder { + + @Override + public Secret buildFor(MySQLSchema schema, Context context) { + return new SecretBuilder() + .withNewMetadata() + .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) + .withNamespace(schema.getMetadata().getNamespace()) + .endMetadata() + .addToData("MYSQL_USERNAME", encode( + context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) + .addToData("MYSQL_PASSWORD", encode( + context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) + .build(); + } + + private static String encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } + } + @Override - public List prepareEventSources( - EventSourceContext context) { - return List.of(new PerResourcePollingEventSource<>( - new SchemaPollingResourceSupplier(mysqlDbConfig), context.getPrimaryCache(), POLL_PERIOD, - Schema.class)); + public void injectInto(EventSourceContext context) { + context.put(MYSQL_DB_CONFIG, mysqlDbConfig); } @Override - public UpdateControl reconcile(MySQLSchema schema, Context context) { - log.info("Reconciling MySQLSchema with name: {}", schema.getMetadata().getName()); - var dbSchema = context.getSecondaryResource(Schema.class); - log.debug("Schema: {} found for: {} ", dbSchema, schema.getMetadata().getName()); - try (Connection connection = getConnection()) { - if (dbSchema.isEmpty()) { - log.debug("Creating Schema and related resources for: {}", schema.getMetadata().getName()); - var schemaName = schema.getMetadata().getName(); - String password = RandomStringUtils.randomAlphanumeric(16); - String secretName = String.format(SECRET_FORMAT, schemaName); - String userName = String.format(USERNAME_FORMAT, schemaName); - - SchemaService.createSchemaAndRelatedUser(connection, schemaName, - schema.getSpec().getEncoding(), userName, password); - createSecret(schema, password, secretName, userName); - updateStatusPojo(schema, secretName, userName); - log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); - return UpdateControl.updateStatus(schema); - } else { - log.debug("No update on MySQLSchema with name: {}", schema.getMetadata().getName()); - return UpdateControl.noUpdate(); - } - } catch (SQLException e) { - log.error("Error while creating Schema", e); - throw new IllegalStateException(e); - } + public void initContext(MySQLSchema primary, Context context) { + final var name = primary.getMetadata().getName(); + // NOSONAR we don't need cryptographically-strong randomness here + final var password = RandomStringUtils.randomAlphanumeric(16); + final var secretName = String.format(SECRET_FORMAT, name); + final var userName = String.format(USERNAME_FORMAT, name); + + // put information in context for other dependents and reconciler to use + context.put(MYSQL_SECRET_PASSWORD, password); + context.put(MYSQL_SECRET_NAME, secretName); + context.put(MYSQL_SECRET_USERNAME, userName); } @Override - public DeleteControl cleanup(MySQLSchema schema, Context context) { - log.info("Cleaning up for: {}", schema.getMetadata().getName()); - try (Connection connection = getConnection()) { - var dbSchema = SchemaService.getSchema(connection, schema.getMetadata().getName()); - if (dbSchema.isPresent()) { - var userName = schema.getStatus() != null ? schema.getStatus().getUserName() : null; - SchemaService.deleteSchemaAndRelatedUser(connection, schema.getMetadata().getName(), - userName); - } else { - log.info( - "Delete event ignored for schema '{}', real schema doesn't exist", - schema.getMetadata().getName()); - } - return DeleteControl.defaultDelete(); - } catch (SQLException e) { - log.error("Error while trying to delete Schema", e); - return DeleteControl.noFinalizerRemoval(); - } + public UpdateControl reconcile(MySQLSchema schema, Context context) { + // we only need to update the status if we just built the schema, i.e. when it's present in the + // context + return context.get(BUILT_SCHEMA, Schema.class).map(s -> { + updateStatusPojo(schema, context.getMandatory(MYSQL_SECRET_NAME, String.class), + context.getMandatory(MYSQL_SECRET_USERNAME, String.class)); + log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); + return UpdateControl.updateStatus(schema); + }).orElse(UpdateControl.noUpdate()); } @Override @@ -110,14 +116,6 @@ public Optional updateErrorStatus(MySQLSchema schema, RetryInfo ret return Optional.empty(); } - private Connection getConnection() throws SQLException { - String connectionString = - format("jdbc:mysql://%1$s:%2$s", mysqlDbConfig.getHost(), mysqlDbConfig.getPort()); - - log.debug("Connecting to '{}' with user '{}'", connectionString, mysqlDbConfig.getUser()); - return DriverManager.getConnection(connectionString, mysqlDbConfig.getUser(), - mysqlDbConfig.getPassword()); - } private void updateStatusPojo(MySQLSchema schema, String secretName, String userName) { SchemaStatus status = new SchemaStatus(); @@ -130,33 +128,4 @@ private void updateStatusPojo(MySQLSchema schema, String secretName, String user status.setStatus("CREATED"); schema.setStatus(status); } - - private void createSecret(MySQLSchema schema, String password, String secretName, - String userName) { - - var currentSecret = kubernetesClient.secrets().inNamespace(schema.getMetadata().getNamespace()) - .withName(secretName).get(); - if (currentSecret != null) { - return; - } - Secret credentialsSecret = - new SecretBuilder() - .withNewMetadata() - .withName(secretName) - .withOwnerReferences(new OwnerReference("mysql.sample.javaoperatorsdk/v1", - false, false, "MySQLSchema", - schema.getMetadata().getName(), schema.getMetadata().getUid())) - .endMetadata() - .addToData( - "MYSQL_USERNAME", Base64.getEncoder().encodeToString(userName.getBytes())) - .addToData( - "MYSQL_PASSWORD", Base64.getEncoder().encodeToString(password.getBytes())) - .build(); - this.kubernetesClient - .secrets() - .inNamespace(schema.getMetadata().getNamespace()) - .create(credentialsSecret); - } - - } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java new file mode 100644 index 0000000000..de4b0ece58 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -0,0 +1,88 @@ +package io.javaoperatorsdk.operator.sample; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.sample.schema.Schema; +import io.javaoperatorsdk.operator.sample.schema.SchemaService; + +import static java.lang.String.format; + +public class SchemaDependentResource + implements DependentResource, Builder, + Cleaner, Persister { + + private static final int POLL_PERIOD = 500; + private MySQLDbConfig dbConfig; + + @Override + public EventSource initEventSource(EventSourceContext context) { + dbConfig = context.getMandatory(MySQLSchemaReconciler.MYSQL_DB_CONFIG, MySQLDbConfig.class); + return new PerResourcePollingEventSource<>( + new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), POLL_PERIOD, + Schema.class); + } + + @Override + public Schema buildFor(MySQLSchema primary, Context context) { + try (Connection connection = getConnection()) { + final var schema = SchemaService.createSchemaAndRelatedUser( + connection, + primary.getMetadata().getName(), + primary.getSpec().getEncoding(), + context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_USERNAME, String.class), + context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_PASSWORD, String.class)); + + // put the newly built schema in the context to let the reconciler know we just built it + context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); + return schema; + } catch (SQLException e) { + MySQLSchemaReconciler.log.error("Error while creating Schema", e); + throw new IllegalStateException(e); + } + } + + private Connection getConnection() throws SQLException { + String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort()); + + MySQLSchemaReconciler.log.debug("Connecting to '{}' with user '{}'", connectURL, + dbConfig.getUser()); + return DriverManager.getConnection(connectURL, dbConfig.getUser(), dbConfig.getPassword()); + } + + @Override + public void delete(Schema fetched, MySQLSchema primary, Context context) { + try (Connection connection = getConnection()) { + var userName = primary.getStatus() != null ? primary.getStatus().getUserName() : null; + SchemaService.deleteSchemaAndRelatedUser(connection, primary.getMetadata().getName(), + userName); + } catch (SQLException e) { + throw new RuntimeException("Error while trying to delete Schema", e); + } + } + + @Override + public void createOrReplace(Schema dependentResource, Context context) { + // this is actually implemented in buildFor, the cleaner way to do this would be to have all + // the needed information in Schema instead of creating both the schema and user from + // heterogeneous information + } + + @Override + public Schema getFor(MySQLSchema primary, Context context) { + try (Connection connection = getConnection()) { + return SchemaService.getSchema(connection, primary.getMetadata().getName()).orElse(null); + } catch (SQLException e) { + throw new RuntimeException("Error while trying to delete Schema", e); + } + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java index 8c6cd31b70..6db1c9b03b 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java @@ -29,7 +29,7 @@ public Optional getSchema(String name) { } } - public static void createSchemaAndRelatedUser(Connection connection, String schemaName, + public static Schema createSchemaAndRelatedUser(Connection connection, String schemaName, String encoding, String userName, String password) { @@ -49,6 +49,8 @@ public static void createSchemaAndRelatedUser(Connection connection, String sche statement.execute( format("GRANT ALL ON `%1$s`.* TO '%2$s'", schemaName, userName)); } + + return new Schema(schemaName, encoding); } catch (SQLException e) { throw new IllegalStateException(e); } 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 f4c529426b..31b5304028 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 @@ -37,7 +37,7 @@ public void test() throws IOException { if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) { Operator operator = new Operator(client, DefaultConfigurationService.instance()); MySQLDbConfig dbConfig = new MySQLDbConfig("mysql", null, "root", "password"); - operator.register(new MySQLSchemaReconciler(client, dbConfig)); + operator.register(new MySQLSchemaReconciler(dbConfig)); operator.start(); } diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 9877aaef0d..e5819f32a8 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index ffcd7038e7..5c75031b16 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java new file mode 100644 index 0000000000..245011a1cf --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -0,0 +1,52 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.KubernetesDependent; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; + +@KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") +public class DeploymentDependentResource + implements DependentResource, Builder, + Updater { + + @Override + public Deployment buildFor(Tomcat tomcat, Context context) { + Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); + final ObjectMeta tomcatMetadata = tomcat.getMetadata(); + final String tomcatName = tomcatMetadata.getName(); + deployment = new DeploymentBuilder(deployment) + .editMetadata() + .withName(tomcatName) + .withNamespace(tomcatMetadata.getNamespace()) + .addToLabels("app", tomcatName) + .addToLabels("app.kubernetes.io/part-of", tomcatName) + .addToLabels("app.kubernetes.io/managed-by", "tomcat-operator") + .endMetadata() + .editSpec() + .editSelector().addToMatchLabels("app", tomcatName).endSelector() + .withReplicas(tomcat.getSpec().getReplicas()) + // set tomcat version + .editTemplate() + // make sure label selector matches label (which has to be matched by service selector too) + .editMetadata().addToLabels("app", tomcatName).endMetadata() + .editSpec() + .editFirstContainer().withImage("tomcat:" + tomcat.getSpec().getVersion()).endContainer() + .endSpec() + .endTemplate() + .endSpec() + .build(); + return deployment; + } + + @Override + public Deployment update(Deployment fetched, Tomcat tomcat, Context context) { + return new DeploymentBuilder(fetched).editSpec().editTemplate().editSpec().editFirstContainer() + .withImage("tomcat:" + tomcat.getSpec().getVersion()) + .endContainer().endSpec().endTemplate().endSpec().build(); + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java new file mode 100644 index 0000000000..b90bd0e6e2 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; + +public class ServiceDependentResource + implements DependentResource, Builder { + + @Override + public Service buildFor(Tomcat tomcat, Context context) { + final ObjectMeta tomcatMetadata = tomcat.getMetadata(); + final Service service = + new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) + .editMetadata() + .withName(tomcatMetadata.getName()) + .withNamespace(tomcatMetadata.getNamespace()) + .endMetadata() + .editSpec() + .addToSelector("app", tomcatMetadata.getName()) + .endSpec() + .build(); + return service; + } +} diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java new file mode 100644 index 0000000000..9dd49466ab --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.Set; +import java.util.stream.Collectors; + +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; + +public class TomcatDependentResource + implements DependentResource, PrimaryResourcesRetriever, + AssociatedSecondaryResourceIdentifier, EventSourceContextAware { + + private ResourceCache primaryCache; + + @Override + public void initWith(EventSourceContext context) { + this.primaryCache = context.getPrimaryCache(); + } + + @Override + public Set associatedPrimaryResources(Tomcat t) { + // To create an event to a related WebApp resource and trigger the reconciliation + // we need to find which WebApp this Tomcat custom resource is related to. + // To find the related customResourceId of the WebApp resource we traverse the cache to + // and identify it based on naming convention. + return primaryCache + .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) + .map(ResourceID::fromResource) + .collect(Collectors.toSet()); + } + + @Override + public ResourceID associatedSecondaryID(Webapp primary) { + return new ResourceID(primary.getSpec().getTomcat(), primary.getMetadata().getNamespace()); + } +} 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 487183dfe5..09a7394e5b 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 @@ -25,7 +25,7 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, DefaultConfigurationService.instance()); - operator.register(new TomcatReconciler(client)); + operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index bb67128f9e..32f7927f44 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -2,28 +2,20 @@ import java.io.IOException; import java.io.InputStream; -import java.util.List; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.informers.SharedIndexInformer; import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.config.Dependent; 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.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -31,45 +23,27 @@ * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also * creates a Service over which the Pods can be accessed. */ -@ControllerConfiguration(finalizerName = NO_FINALIZER) -public class TomcatReconciler implements Reconciler, EventSourceInitializer { +@ControllerConfiguration( + finalizerName = NO_FINALIZER, + dependents = { + @Dependent(resourceType = Deployment.class, type = DeploymentDependentResource.class), + @Dependent(resourceType = Service.class, type = ServiceDependentResource.class) + }) +public class TomcatReconciler implements Reconciler { private final Logger log = LoggerFactory.getLogger(getClass()); - private final KubernetesClient kubernetesClient; - - public TomcatReconciler(KubernetesClient client) { - this.kubernetesClient = client; - } - - @Override - public List prepareEventSources(EventSourceContext context) { - SharedIndexInformer deploymentInformer = - kubernetesClient.apps().deployments().inAnyNamespace() - .withLabel("app.kubernetes.io/managed-by", "tomcat-operator") - .runnableInformer(0); - - return List.of(new InformerEventSource<>( - deploymentInformer, Mappers.fromOwnerReference())); - } - @Override public UpdateControl reconcile(Tomcat tomcat, Context context) { - createOrUpdateDeployment(tomcat); - createOrUpdateService(tomcat); - - return context.getSecondaryResource(Deployment.class) - .map(deployment -> { - Tomcat updatedTomcat = - updateTomcatStatus(tomcat, deployment); - log.info( - "Updating status of Tomcat {} in namespace {} to {} ready replicas", - tomcat.getMetadata().getName(), - tomcat.getMetadata().getNamespace(), - tomcat.getStatus().getReadyReplicas()); - return UpdateControl.updateStatus(updatedTomcat); - }) - .orElse(UpdateControl.noUpdate()); + return context.getSecondaryResource(Deployment.class).map(deployment -> { + Tomcat updatedTomcat = updateTomcatStatus(tomcat, deployment); + log.info( + "Updating status of Tomcat {} in namespace {} to {} ready replicas", + tomcat.getMetadata().getName(), + tomcat.getMetadata().getNamespace(), + tomcat.getStatus().getReadyReplicas()); + return UpdateControl.updateStatus(updatedTomcat); + }).orElse(UpdateControl.noUpdate()); } private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { @@ -82,78 +56,8 @@ private Tomcat updateTomcatStatus(Tomcat tomcat, Deployment deployment) { return tomcat; } - private void createOrUpdateDeployment(Tomcat tomcat) { - String ns = tomcat.getMetadata().getNamespace(); - Deployment existingDeployment = - kubernetesClient - .apps() - .deployments() - .inNamespace(ns) - .withName(tomcat.getMetadata().getName()) - .get(); - if (existingDeployment == null) { - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); - deployment.getMetadata().setName(tomcat.getMetadata().getName()); - deployment.getMetadata().setNamespace(ns); - deployment.getMetadata().getLabels().put("app.kubernetes.io/part-of", - tomcat.getMetadata().getName()); - deployment.getMetadata().getLabels().put("app.kubernetes.io/managed-by", "tomcat-operator"); - // set tomcat version - deployment - .getSpec() - .getTemplate() - .getSpec() - .getContainers() - .get(0) - .setImage("tomcat:" + tomcat.getSpec().getVersion()); - deployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); - - // make sure label selector matches label (which has to be matched by service selector too) - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", tomcat.getMetadata().getName()); - deployment - .getSpec() - .getSelector() - .getMatchLabels() - .put("app", tomcat.getMetadata().getName()); - - OwnerReference ownerReference = deployment.getMetadata().getOwnerReferences().get(0); - ownerReference.setName(tomcat.getMetadata().getName()); - ownerReference.setUid(tomcat.getMetadata().getUid()); - - log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); - kubernetesClient.apps().deployments().inNamespace(ns).create(deployment); - } else { - existingDeployment - .getSpec() - .getTemplate() - .getSpec() - .getContainers() - .get(0) - .setImage("tomcat:" + tomcat.getSpec().getVersion()); - existingDeployment.getSpec().setReplicas(tomcat.getSpec().getReplicas()); - kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(existingDeployment); - } - } - - private void createOrUpdateService(Tomcat tomcat) { - Service service = loadYaml(Service.class, "service.yaml"); - service.getMetadata().setName(tomcat.getMetadata().getName()); - String ns = tomcat.getMetadata().getNamespace(); - service.getMetadata().setNamespace(ns); - service.getMetadata().getOwnerReferences().get(0).setName(tomcat.getMetadata().getName()); - service.getMetadata().getOwnerReferences().get(0).setUid(tomcat.getMetadata().getUid()); - service.getSpec().getSelector().put("app", tomcat.getMetadata().getName()); - log.info("Creating or updating Service {} in {}", service.getMetadata().getName(), ns); - kubernetesClient.services().inNamespace(ns).createOrReplace(service); - } - - private T loadYaml(Class clazz, String yaml) { - try (InputStream is = getClass().getResourceAsStream(yaml)) { + static T loadYaml(Class clazz, String yaml) { + try (InputStream is = TomcatReconciler.class.getResourceAsStream(yaml)) { return Serialization.unmarshal(is, clazz); } catch (IOException ex) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 0f785feb02..9e1f6f677f 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -25,15 +25,18 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; @ControllerConfiguration public class WebappReconciler implements Reconciler, EventSourceInitializer { - private KubernetesClient kubernetesClient; + private static final Logger log = LoggerFactory.getLogger(WebappReconciler.class); - private final Logger log = LoggerFactory.getLogger(getClass()); + private final KubernetesClient kubernetesClient; public WebappReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; @@ -41,20 +44,32 @@ public WebappReconciler(KubernetesClient kubernetesClient) { @Override public List prepareEventSources(EventSourceContext context) { - return List.of(new InformerEventSource<>( - kubernetesClient, Tomcat.class, t -> { - // To create an event to a related WebApp resource and trigger the reconciliation - // we need to find which WebApp this Tomcat custom resource is related to. - // To find the related customResourceId of the WebApp resource we traverse the cache to - // and identify it based on naming convention. - return context.getPrimaryCache() - .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) - .map(ResourceID::fromResource) - .collect(Collectors.toSet()); - }, - (Webapp webapp) -> new ResourceID(webapp.getSpec().getTomcat(), - webapp.getMetadata().getNamespace()), - true)); + /* + * To create an event to a related WebApp resource and trigger the reconciliation we need to + * find which WebApp this Tomcat custom resource is related to. To find the related + * customResourceId of the WebApp resource we traverse the cache and identify it based on naming + * convention. + */ + final PrimaryResourcesRetriever webappsMatchingTomcatName = + (Tomcat t) -> context.getPrimaryCache() + .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) + .map(ResourceID::fromResource) + .collect(Collectors.toSet()); + + /* + * We retrieve the Tomcat instance associated with out Webapp from its spec + */ + final AssociatedSecondaryResourceIdentifier tomcatFromWebAppSpec = + (Webapp webapp) -> new ResourceID( + webapp.getSpec().getTomcat(), + webapp.getMetadata().getNamespace()); + + InformerConfiguration configuration = + InformerConfiguration.from(context, Tomcat.class) + .withPrimaryResourcesRetriever(webappsMatchingTomcatName) + .withAssociatedSecondaryResourceIdentifier(tomcatFromWebAppSpec) + .build(); + return List.of(new InformerEventSource<>(configuration, context)); } /** @@ -144,7 +159,7 @@ private String[] executeCommandInAllPods( CompletableFuture data = new CompletableFuture<>(); try (ExecWatch execWatch = execCmd(pod, data, command)) { - status[i] = "" + pod.getMetadata().getName() + ":" + data.get(30, TimeUnit.SECONDS);; + status[i] = "" + pod.getMetadata().getName() + ":" + data.get(30, TimeUnit.SECONDS); } catch (ExecutionException e) { status[i] = "" + pod.getMetadata().getName() + ": ExecutionException - " + e.getMessage(); } catch (InterruptedException e) { @@ -194,7 +209,7 @@ public void onFailure(Throwable t, Response response) { @Override public void onClose(int code, String reason) { - log.debug("Exit with: " + code + " and with reason: " + reason); + log.debug("Exit with: {} and with reason: {}", code, reason); data.complete(baos.toString()); } } diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml index 55d1a6be7d..aa38eb3619 100644 --- a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/deployment.yaml @@ -5,11 +5,6 @@ metadata: labels: app.kubernetes.io/part-of: "" app.kubernetes.io/managed-by: "" # used for filtering of Deployments created by the controller - ownerReferences: # used for finding which Tomcat does this Deployment belong to - - apiVersion: apps/v1 - kind: Tomcat - name: "" - uid: "" spec: selector: matchLabels: diff --git a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml index a807d277a7..ab198643ed 100644 --- a/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml +++ b/sample-operators/tomcat-operator/src/main/resources/io/javaoperatorsdk/operator/sample/service.yaml @@ -2,11 +2,6 @@ apiVersion: v1 kind: Service metadata: name: "" - ownerReferences: # used for finding which Tomcat does this Deployment belong to - - apiVersion: apps/v1 - kind: Tomcat - name: "" - uid: "" spec: selector: app: "" diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index e803b70aba..9f80f31c5b 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -33,7 +33,7 @@ public void test() { // Use this if you want to run the test without deploying the Operator to Kubernetes if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) { Operator operator = new Operator(client, DefaultConfigurationService.instance()); - operator.register(new TomcatReconciler(client)); + operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); operator.start(); } diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 90522733af..94e4dbcb83 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT webpage @@ -25,7 +25,6 @@ io.javaoperatorsdk operator-framework - 2.0.3-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index db2926c74b..96cd79f960 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 3dcdb530c7..02aae34d54 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 87ccbbf48d..0b26c7de42 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index b652916960..c41e3c1cfc 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 22f354741cc059ac9e05dcb0ed0d79db7dad11f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 28 Jan 2022 10:29:41 +0100 Subject: [PATCH 0261/1608] fix: change log level of no cr found (#873) It's can happen that after a custom resource deleted, event sources, normally also informers still receive events about dependent resources deleted. --- .../operator/processing/event/EventProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 33c2b7645b..29f9adab55 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 @@ -158,7 +158,7 @@ private void submitReconciliationExecution(ResourceID resourceID) { controllerUnderExecution, latest.isPresent()); if (latest.isEmpty()) { - log.warn("no custom resource found in cache for ResourceID: {}", resourceID); + log.debug("no custom resource found in cache for ResourceID: {}", resourceID); } } } finally { From 3986343f2398663b25dac8033aa52fd6bc3a62b5 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 25 Jan 2022 13:47:13 +0100 Subject: [PATCH 0262/1608] feat: add @Ignore annotation to mark internal Reconciler implementations (#863) This allows downstream tools to safely ignore the annotated Reconciler when considering which implementations to process. Fixes #862 --- .../operator/api/reconciler/Ignore.java | 17 +++++++++++++++++ .../operator/processing/Controller.java | 2 ++ .../dependent/DependentResourceManager.java | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Ignore.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Ignore.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Ignore.java new file mode 100644 index 0000000000..ad2a755db8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Ignore.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation for downstream tooling to ignore the annotated {@link Reconciler}. This allows to + * mark some implementations as not provided by user and should therefore be ignored by processes + * external to the SDK itself. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Ignore { + +} 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 e0b9efae77..be819c5621 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 @@ -27,6 +27,7 @@ import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.dependent.DependentResourceManager; @@ -34,6 +35,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"rawtypes", "unchecked"}) +@Ignore public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 655113d251..30e92e3d63 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -15,6 +15,7 @@ import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; @@ -23,6 +24,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"rawtypes", "unchecked"}) +@Ignore public class DependentResourceManager implements EventSourceInitializer, EventSourceContextInjector, Reconciler { From d38dfb79ed02db3929dbfffc15f8cdd6c3f16df6 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 28 Jan 2022 13:04:31 +0100 Subject: [PATCH 0263/1608] refactor: clean up configurations (#876) --- .../DefaultControllerConfiguration.java | 43 +++---------------- .../config/DefaultResourceConfiguration.java | 3 +- ...ernetesDependentResourceConfiguration.java | 38 ++++++++++++++++ ...KubernetesDependentResourceController.java | 12 +++--- .../informer/InformerConfiguration.java | 2 +- .../runtime/AnnotationConfiguration.java | 4 +- 6 files changed, 55 insertions(+), 47 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java 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 index bc10002ebd..0a3c68c70b 100644 --- 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 @@ -8,6 +8,7 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class DefaultControllerConfiguration + extends DefaultResourceConfiguration implements ControllerConfiguration { private final String associatedControllerClassName; @@ -15,14 +16,9 @@ public class DefaultControllerConfiguration private final String crdName; private final String finalizer; private final boolean generationAware; - private final Set namespaces; - private final boolean watchAllNamespaces; private final RetryConfiguration retryConfiguration; - private final String labelSelector; private final ResourceEventFilter resourceEventFilter; - private final Class resourceClass; private final List dependents; - private ConfigurationService service; // NOSONAR constructor is meant to provide all information public DefaultControllerConfiguration( @@ -38,23 +34,18 @@ public DefaultControllerConfiguration( Class resourceClass, ConfigurationService service, List dependents) { + super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; this.name = name; this.crdName = crdName; this.finalizer = finalizer; this.generationAware = generationAware; - this.namespaces = - namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); - this.watchAllNamespaces = this.namespaces.isEmpty(); this.retryConfiguration = retryConfiguration == null ? ControllerConfiguration.super.getRetryConfiguration() : retryConfiguration; - this.labelSelector = labelSelector; this.resourceEventFilter = resourceEventFilter; - this.resourceClass = - resourceClass == null ? ControllerConfiguration.super.getResourceClass() - : resourceClass; + setConfigurationService(service); this.dependents = dependents != null ? dependents : Collections.emptyList(); } @@ -84,43 +75,19 @@ public String getAssociatedReconcilerClassName() { return associatedControllerClassName; } - @Override - public Set getNamespaces() { - return namespaces; - } - - @Override - public boolean watchAllNamespaces() { - return watchAllNamespaces; - } - @Override public RetryConfiguration getRetryConfiguration() { return retryConfiguration; } - @Override - public ConfigurationService getConfigurationService() { - return service; - } @Override public void setConfigurationService(ConfigurationService service) { - if (this.service != null) { + if (getConfigurationService() != null) { throw new IllegalStateException("A ConfigurationService is already associated with '" + name + "' ControllerConfiguration. Cannot change it once set!"); } - this.service = service; - } - - @Override - public String getLabelSelector() { - return labelSelector; - } - - @Override - public Class getResourceClass() { - return resourceClass; + super.setConfigurationService(service); } @Override 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 d7b5f76815..7b80416433 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 @@ -23,7 +23,8 @@ public DefaultResourceConfiguration(String labelSelector, Class resourceClass public DefaultResourceConfiguration(String labelSelector, Class resourceClass, Set namespaces) { this.labelSelector = labelSelector; - this.resourceClass = resourceClass; + this.resourceClass = resourceClass == null ? ResourceConfiguration.super.getResourceClass() + : resourceClass; this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); this.watchAllNamespaces = this.namespaces.isEmpty(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java new file mode 100644 index 0000000000..dad9411d2d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Set; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; + +public class KubernetesDependentResourceConfiguration + extends InformerConfiguration { + + private final boolean owned; + + protected KubernetesDependentResourceConfiguration( + ConfigurationService service, + String labelSelector, Class resourceClass, + PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange, Set namespaces, boolean owned) { + super(service, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, + skipUpdateEventPropagationIfNoChange, namespaces); + this.owned = owned; + } + + public static KubernetesDependentResourceConfiguration from( + InformerConfiguration cfg, boolean owned) { + return new KubernetesDependentResourceConfiguration<>(cfg.getConfigurationService(), + cfg.getLabelSelector(), cfg.getResourceClass(), cfg.getPrimaryResourcesRetriever(), + cfg.getAssociatedResourceIdentifier(), cfg.isSkipUpdateEventPropagationIfNoChange(), + cfg.getNamespaces(), owned); + } + + public boolean isOwned() { + return owned; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java index da11c4f938..015b60f4bb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java @@ -13,14 +13,13 @@ public class KubernetesDependentResourceController extends DependentResourceController { - private final InformerConfiguration configuration; - private final boolean owned; + private final KubernetesDependentResourceConfiguration configuration; private KubernetesClient client; private InformerEventSource informer; public KubernetesDependentResourceController(DependentResource delegate, - InformerConfiguration configuration, boolean owned) { + KubernetesDependentResourceConfiguration configuration) { super(delegate); // todo: check if we can validate that types actually match properly final var associatedPrimaries = @@ -32,11 +31,12 @@ public KubernetesDependentResourceController(DependentResource delegate, ? (AssociatedSecondaryResourceIdentifier

) delegate : configuration.getAssociatedResourceIdentifier(); - this.configuration = InformerConfiguration.from(configuration) + final var augmented = InformerConfiguration.from(configuration) .withPrimaryResourcesRetriever(associatedPrimaries) .withAssociatedSecondaryResourceIdentifier(associatedSecondary) .build(); - this.owned = owned; + this.configuration = + KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned()); } @Override @@ -69,6 +69,6 @@ public R getFor(P primary, Context context) { } public boolean owned() { - return owned; + return configuration.isOwned(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java index 10bfdcdcb8..0c06698327 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java @@ -19,7 +19,7 @@ public class InformerConfiguration private final AssociatedSecondaryResourceIdentifier

associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; - private InformerConfiguration(ConfigurationService service, String labelSelector, + protected InformerConfiguration(ConfigurationService service, String labelSelector, Class resourceClass, PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, AssociatedSecondaryResourceIdentifier

associatedWith, diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 89a100741f..30d2600cfe 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -15,6 +15,7 @@ import io.javaoperatorsdk.operator.api.config.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -153,7 +154,8 @@ public List getDependentResources() { .skippingEventPropagationIfUnchanged(skipIfUnchanged) .withNamespaces(namespaces) .build(); - dependent = new KubernetesDependentResourceController(dependent, configuration, owned); + dependent = new KubernetesDependentResourceController(dependent, + KubernetesDependentResourceConfiguration.from(configuration, owned)); } dependents.add(dependent); From e9d0b2f834423e9f4ec735309b8887a79797bc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 28 Jan 2022 13:15:55 +0100 Subject: [PATCH 0264/1608] docs: improvemetn on observede generation (#880) --- docs/documentation/features.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 8edfec98e5..aa5af6a61d 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -198,8 +198,9 @@ in status) to handle generation filtering in memory. Thus, if an event is receiv resource is compared with the resource in the cache. Note that the **first approach has significant benefits** in the situation when the operator is restarted and there is -no cached resource anymore. In case two this leads to a reconciliation of every resource, event if the resource is not -changed while the operator was not running. +no cached resource anymore. In case two this leads to a reconciliation of every resource in all cases, +event if the resource is not changed while the operator was not running. However, in case informers are used +the reconciliation from startup will happen anyway, since events will be propagated by the informer. ## Support for Well Known (non-custom) Kubernetes Resources From 59ce66fb43d4b8175d81776ddeacba7d610d4d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 28 Jan 2022 15:26:51 +0100 Subject: [PATCH 0265/1608] docs: Spring Boot Starter link (#881) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ffc36dc7c0..e41067e9a0 100644 --- a/README.md +++ b/README.md @@ -44,5 +44,5 @@ Operator SDK plugin: https://github.com/operator-framework/java-operator-plugins Quarkus Extension: https://github.com/quarkiverse/quarkus-operator-sdk - +Spring Boot Starter: https://github.com/java-operator-sdk/operator-framework-spring-boot-starter From 7658e31410296b996764ff5aa01d0c4e77ac8160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 31 Jan 2022 15:04:26 +0100 Subject: [PATCH 0266/1608] feature: controller reconciliation max delay (#871) --- docs/documentation/features.md | 28 ++++++++++-- .../api/config/ControllerConfiguration.java | 6 +++ .../ControllerConfigurationOverrider.java | 11 +++++ .../DefaultControllerConfiguration.java | 10 +++++ .../operator/api/reconciler/Constants.java | 1 + .../reconciler/ControllerConfiguration.java | 5 +++ .../reconciler/ReconciliationMaxInterval.java | 36 +++++++++++++++ .../event/ReconciliationDispatcher.java | 5 ++- .../operator/ControllerManagerTest.java | 3 +- .../event/ReconciliationDispatcherTest.java | 40 ++++++++++++++++- .../event/source/ResourceEventFilterTest.java | 2 +- .../ControllerResourceEventSourceTest.java | 1 + .../operator/junit/OperatorExtension.java | 6 +++ .../runtime/AnnotationConfiguration.java | 17 +++++++ .../operator/ConcurrencyIT.java | 4 +- .../operator/ControllerExecutionIT.java | 6 +-- .../operator/CustomResourceFilterIT.java | 4 +- .../operator/ErrorStatusHandlerIT.java | 4 +- .../operator/EventSourceIT.java | 4 +- .../operator/InformerEventSourceIT.java | 2 +- .../KubernetesResourceStatusUpdateIT.java | 4 +- .../operator/MaxIntervalIT.java | 45 +++++++++++++++++++ .../ObservedGenerationHandlingIT.java | 4 +- .../io/javaoperatorsdk/operator/RetryIT.java | 4 +- .../operator/RetryMaxAttemptIT.java | 4 +- .../operator/SubResourceUpdateIT.java | 10 ++--- .../operator/UpdatingResAndSubResIT.java | 4 +- .../MaxIntervalTestCustomResource.java | 17 +++++++ .../MaxIntervalTestCustomResourceStatus.java | 5 +++ .../MaxIntervalTestReconciler.java | 30 +++++++++++++ 30 files changed, 288 insertions(+), 34 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index aa5af6a61d..9b83bb4348 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -160,9 +160,7 @@ larger than the `.observedGeneration` field on status. In order to have this fea . If these conditions are fulfilled and generation awareness not turned off, the observed generation is automatically set -by the framework after the `reconcile` method is called. There is just one exception, when the reconciler returns -with `UpdateControl.updateResource`, in this case the status is not updated, just the custom resource - however, this -update will lead to a new reconciliation. Note that the observed generation is updated also +by the framework after the `reconcile` method is called. Note that the observed generation is updated also when `UpdateControl.noUpdate()` is returned from the reconciler. See this feature working in the [WebPage example](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java#L5) . @@ -224,6 +222,30 @@ public class DeploymentReconciler } ``` +## Max Interval Between Reconciliations + +In case informers are all in place and reconciler is implemented correctly, there is no need for additional triggers. +However, it's a [common practice](https://github.com/java-operator-sdk/java-operator-sdk/issues/848#issuecomment-1016419966) +to have a failsafe periodic trigger in place, +just to make sure the resources are reconciled after certain time. This functionality is in place by default, there +is quite high interval (currently 10 hours) while the reconciliation is triggered. See how to override this using +the standard annotation: + +```java +@ControllerConfiguration(finalizerName = NO_FINALIZER, + reconciliationMaxInterval = @ReconciliationMaxInterval( + interval = 50, + timeUnit = TimeUnit.MILLISECONDS)) +``` + +The event is not propagated in a fixed rate, rather it's scheduled after each reconciliation. So the +next reconciliation will after at most within the specified interval after last reconciliation. + +This feature can be turned off by setting `reconciliationMaxInterval` to [`Constants.NO_RECONCILIATION_MAX_INTERVAL`](https://github.com/java-operator-sdk/java-operator-sdk/blob/442e7d8718e992a36880e42bd0a5c01affaec9df/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java#L8-L8) +or any non-positive number. + +The automatic retries are not affected by this feature, in case of an error no schedule is set by this feature. + ## Automatic Retries on Error When an exception is thrown from a controller, the framework will schedule an automatic retry of the reconciliation. The 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 b85df49eba..eb247752aa 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 @@ -1,7 +1,9 @@ package io.javaoperatorsdk.operator.api.config; import java.lang.reflect.ParameterizedType; +import java.time.Duration; import java.util.Collections; +import java.util.Optional; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -114,4 +116,8 @@ default boolean useFinalizer() { default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } + + default Optional reconciliationMaxInterval() { + return Optional.of(Duration.ofHours(10L)); + } } 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 e8e2ef1162..c018c369f0 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,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.time.Duration; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -16,6 +17,7 @@ public class ControllerConfigurationOverrider { private String labelSelector; private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; + private Duration reconciliationMaxInterval; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizer(); @@ -24,7 +26,9 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { retry = original.getRetryConfiguration(); labelSelector = original.getLabelSelector(); customResourcePredicate = original.getEventFilter(); + reconciliationMaxInterval = original.reconciliationMaxInterval().orElse(null); this.original = original; + } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -74,6 +78,12 @@ public ControllerConfigurationOverrider withCustomResourcePredicate( return this; } + public ControllerConfigurationOverrider withReconciliationMaxInterval( + Duration reconciliationMaxInterval) { + this.reconciliationMaxInterval = reconciliationMaxInterval; + return this; + } + public ControllerConfiguration build() { return new DefaultControllerConfiguration<>( original.getAssociatedReconcilerClassName(), @@ -86,6 +96,7 @@ public ControllerConfiguration build() { labelSelector, customResourcePredicate, original.getResourceClass(), + reconciliationMaxInterval, original.getConfigurationService()); } 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 index 860152745b..33ff56899e 100644 --- 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 @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.api.config; +import java.time.Duration; import java.util.Collections; +import java.util.Optional; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -20,6 +22,7 @@ public class DefaultControllerConfiguration private final String labelSelector; private final ResourceEventFilter resourceEventFilter; private final Class resourceClass; + private final Duration reconciliationMaxInterval; private ConfigurationService service; public DefaultControllerConfiguration( @@ -33,6 +36,7 @@ public DefaultControllerConfiguration( String labelSelector, ResourceEventFilter resourceEventFilter, Class resourceClass, + Duration reconciliationMaxInterval, ConfigurationService service) { this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -41,6 +45,7 @@ public DefaultControllerConfiguration( this.generationAware = generationAware; this.namespaces = namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); + this.reconciliationMaxInterval = reconciliationMaxInterval; this.watchAllNamespaces = this.namespaces.isEmpty(); this.retryConfiguration = retryConfiguration == null @@ -122,4 +127,9 @@ public Class getResourceClass() { public ResourceEventFilter getEventFilter() { return resourceEventFilter; } + + @Override + public Optional reconciliationMaxInterval() { + return Optional.ofNullable(reconciliationMaxInterval); + } } 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 075b4e79a3..85b3a00807 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 @@ -5,6 +5,7 @@ public final class Constants { public static final String EMPTY_STRING = ""; public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; public static final String NO_FINALIZER = "JOSDK_NO_FINALIZER"; + public static final long NO_RECONCILIATION_MAX_INTERVAL = -1L; private Constants() {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java index fad4bc8573..bd8863cad1 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 @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @@ -56,4 +57,8 @@ */ @SuppressWarnings("rawtypes") Class[] eventFilters() default {}; + + ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMaxInterval( + interval = 10, timeUnit = TimeUnit.HOURS); + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java new file mode 100644 index 0000000000..05459e7123 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface ReconciliationMaxInterval { + + /** + * A max delay between two reconciliations. Having this value larger than zero, will ensure that a + * reconciliation is scheduled with a target interval after the last reconciliation. Note that + * this not applies for retries, in case of an exception reconciliation is not scheduled. This is + * not a fixed rate, in other words a new reconciliation is scheduled after each reconciliation. + *

+ * If an interval is specified by {@link UpdateControl} or {@link DeleteControl}, those take + * precedence. + *

+ * This is a fail-safe feature, in the sense that if informers are in place and the reconciler + * implementation is correct, this feature can be turned off. + *

+ * Use NO_RECONCILIATION_MAX_INTERVAL in {@link Constants} to turn off this feature. + * + * @return max delay between reconciliations + **/ + long interval(); + + /** + * @return time unit for max delay between reconciliations + */ + TimeUnit timeUnit() default TimeUnit.HOURS; + +} 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 52374c5645..02c8f8cbb3 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 @@ -245,9 +245,12 @@ private PostExecutionControl createPostExecutionControl(R updatedCustomResour private void updatePostExecutionControlWithReschedule( PostExecutionControl postExecutionControl, BaseControl baseControl) { - baseControl.getScheduleDelay().ifPresent(postExecutionControl::withReSchedule); + baseControl.getScheduleDelay().ifPresentOrElse(postExecutionControl::withReSchedule, + () -> controller.getConfiguration().reconciliationMaxInterval() + .ifPresent(m -> postExecutionControl.withReSchedule(m.toMillis()))); } + private PostExecutionControl handleCleanup(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", 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 a1cfa3e82d..19f65a9441 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 @@ -60,7 +60,8 @@ private static class TestControllerConfiguration public TestControllerConfiguration(Reconciler controller, Class crClass) { super(null, getControllerName(controller), - CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, null); + CustomResource.getCRDName(crClass), null, false, null, null, null, null, crClass, + null, null); this.controller = controller; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcherTest.java index 9da51337d3..8e4be5e6dc 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 @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import java.time.Duration; import java.util.ArrayList; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -47,6 +48,7 @@ class ReconciliationDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; public static final String ERROR_MESSAGE = "ErrorMessage"; + public static final long RECONCILIATION_MAX_INTERVAL = 10L; private TestCustomResource testCustomResource; private ReconciliationDispatcher reconciliationDispatcher; private final Reconciler reconciler = mock(Reconciler.class, @@ -54,6 +56,7 @@ class ReconciliationDispatcherTest { private final ConfigurationService configService = mock(ConfigurationService.class); private final CustomResourceFacade customResourceFacade = mock(ReconciliationDispatcher.CustomResourceFacade.class); + private ControllerConfiguration configuration = mock(ControllerConfiguration.class); @BeforeEach void setup() { @@ -63,17 +66,23 @@ void setup() { } private ReconciliationDispatcher init(R customResource, - Reconciler reconciler, ControllerConfiguration configuration, + Reconciler reconciler, ControllerConfiguration configuration, CustomResourceFacade customResourceFacade, boolean useFinalizer) { + configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; + ReconciliationDispatcherTest.this.configuration = configuration; final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER; when(configuration.getFinalizer()).thenReturn(finalizer); when(configuration.useFinalizer()).thenCallRealMethod(); when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configuration.getResourceClass()).thenReturn((Class) customResource.getClass()); when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); + when(configuration.reconciliationMaxInterval()) + .thenReturn(Optional.of(Duration.ofHours(RECONCILIATION_MAX_INTERVAL))); + when(configuration.getConfigurationService()).thenReturn(configService); + /* * We need this for mock reconcilers to properly generate the expected UpdateControl: without * this, calls such as `when(reconciler.reconcile(eq(testCustomResource), @@ -429,6 +438,35 @@ void callErrorStatusHandlerEvenOnFirstError() { any(), any()); } + @Test + void schedulesReconciliationIfMaxDelayIsSet() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + + when(reconciler.reconcile(eq(testCustomResource), any())) + .thenReturn(UpdateControl.noUpdate()); + + PostExecutionControl control = + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + + assertThat(control.getReScheduleDelay()).isPresent() + .hasValue(TimeUnit.HOURS.toMillis(RECONCILIATION_MAX_INTERVAL)); + } + + @Test + void canSkipSchedulingMaxDelayIf() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + + when(reconciler.reconcile(eq(testCustomResource), any())) + .thenReturn(UpdateControl.noUpdate()); + when(configuration.reconciliationMaxInterval()) + .thenReturn(Optional.empty()); + + PostExecutionControl control = + reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); + + assertThat(control.getReScheduleDelay()).isNotPresent(); + } + private ObservedGenCustomResource createObservedGenCustomResource() { ObservedGenCustomResource observedGenCustomResource = new ObservedGenCustomResource(); observedGenCustomResource.setMetadata(new ObjectMeta()); 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 17168876a5..433614fe17 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 @@ -171,7 +171,7 @@ public ControllerConfig(String finalizer, boolean generationAware, null, eventFilter, customResourceClass, - null); + null, null); } } 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 5cdc85c553..5859115ee2 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 @@ -170,6 +170,7 @@ public TestConfiguration(boolean generationAware) { null, null, TestCustomResource.class, + null, null); } } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 5941975a71..3393de7a55 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -112,6 +112,12 @@ public List getReconcilers() { .collect(Collectors.toUnmodifiableList()); } + public Reconciler getFirstReconciler() { + return operator.getControllers().stream() + .map(Controller::getReconciler) + .findFirst().orElseThrow(); + } + @SuppressWarnings({"rawtypes"}) public T getControllerOfType(Class type) { return operator.getControllers().stream() diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 1000cb61c4..8977bf3ff8 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.time.Duration; +import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -108,6 +110,21 @@ public ResourceEventFilter getEventFilter() { : ResourceEventFilters.passthrough(); } + + + @Override + public Optional reconciliationMaxInterval() { + if (annotation.reconciliationMaxInterval() != null) { + if (annotation.reconciliationMaxInterval().interval() <= 0) { + return Optional.empty(); + } + return Optional.of(Duration.of(annotation.reconciliationMaxInterval().interval(), + annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); + } else { + return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.reconciliationMaxInterval(); + } + } + public static T valueOrDefault(ControllerConfiguration controllerConfiguration, Function mapper, T defaultValue) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index 458e2d12eb..12cf3ca4d8 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class ConcurrencyIT { +class ConcurrencyIT { public static final int NUMBER_OF_RESOURCES_CREATED = 50; public static final int NUMBER_OF_RESOURCES_DELETED = 30; public static final int NUMBER_OF_RESOURCES_UPDATED = 20; @@ -34,7 +34,7 @@ public class ConcurrencyIT { .build(); @Test - public void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedException { + void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedException { log.info("Creating {} new resources", NUMBER_OF_RESOURCES_CREATED); for (int i = 0; i < NUMBER_OF_RESOURCES_CREATED; i++) { TestCustomResource tcr = TestUtils.testCustomResourceWithPrefix(String.valueOf(i)); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index b1f783b958..dbd015587e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -15,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class ControllerExecutionIT { +class ControllerExecutionIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -24,7 +24,7 @@ public class ControllerExecutionIT { .build(); @Test - public void configMapGetsCreatedForTestCustomResource() { + void configMapGetsCreatedForTestCustomResource() { operator.getControllerOfType(TestReconciler.class).setUpdateStatus(true); TestCustomResource resource = TestUtils.testCustomResource(); @@ -36,7 +36,7 @@ public void configMapGetsCreatedForTestCustomResource() { } @Test - public void eventIsSkippedChangedOnMetadataOnlyUpdate() { + void eventIsSkippedChangedOnMetadataOnlyUpdate() { operator.getControllerOfType(TestReconciler.class).setUpdateStatus(false); TestCustomResource resource = TestUtils.testCustomResource(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java index 876710ff36..70a3f04777 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java @@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class CustomResourceFilterIT { +class CustomResourceFilterIT { @RegisterExtension OperatorExtension operator = @@ -22,7 +22,7 @@ public class CustomResourceFilterIT { .build(); @Test - public void doesCustomFiltering() throws InterruptedException { + void doesCustomFiltering() throws InterruptedException { var filtered1 = createTestResource("filtered1", true, false); var filtered2 = createTestResource("filtered2", false, true); var notFiltered = createTestResource("notfiltered", true, true); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java index 4a1b3293e7..fbf54a1ad1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java @@ -15,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class ErrorStatusHandlerIT { +class ErrorStatusHandlerIT { public static final int MAX_RETRY_ATTEMPTS = 3; ErrorStatusHandlerTestReconciler reconciler = new ErrorStatusHandlerTestReconciler(); @@ -29,7 +29,7 @@ public class ErrorStatusHandlerIT { .build(); @Test - public void testErrorMessageSetEventually() { + void testErrorMessageSetEventually() { ErrorStatusHandlerTestCustomResource resource = operator.create(ErrorStatusHandlerTestCustomResource.class, createCustomResource()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index 033bbf2b8b..c91ebaad43 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -16,7 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class EventSourceIT { +class EventSourceIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -25,7 +25,7 @@ public class EventSourceIT { .build(); @Test - public void receivingPeriodicEvents() { + void receivingPeriodicEvents() { EventSourceTestCustomResource resource = createTestCustomResource("1"); operator.create(EventSourceTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 4885e7d356..7ff36bd375 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; -public class InformerEventSourceIT { +class InformerEventSourceIT { public static final String RESOURCE_NAME = "informertestcr"; public static final String INITIAL_STATUS_MESSAGE = "Initial Status"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java index 2c5ea1cba1..a2c7378ff5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class KubernetesResourceStatusUpdateIT { +class KubernetesResourceStatusUpdateIT { @RegisterExtension OperatorExtension operator = @@ -29,7 +29,7 @@ public class KubernetesResourceStatusUpdateIT { .build(); @Test - public void testReconciliationOfNonCustomResourceAndStatusUpdate() { + void testReconciliationOfNonCustomResourceAndStatusUpdate() { var deployment = operator.create(Deployment.class, testDeployment()); await().atMost(120, TimeUnit.SECONDS).untilAsserted(() -> { var d = operator.get(Deployment.class, deployment.getMetadata().getName()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java new file mode 100644 index 0000000000..f284dbf407 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java @@ -0,0 +1,45 @@ +package io.javaoperatorsdk.operator; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.maxinterval.MaxIntervalTestCustomResource; +import io.javaoperatorsdk.operator.sample.maxinterval.MaxIntervalTestReconciler; + +import static org.awaitility.Awaitility.await; + +class MaxIntervalIT { + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new MaxIntervalTestReconciler()) + .build(); + + @Test + void reconciliationTriggeredBasedOnMaxInterval() { + MaxIntervalTestCustomResource cr = createTestResource(); + + operator.create(MaxIntervalTestCustomResource.class, cr); + + await() + .pollInterval(50, TimeUnit.MILLISECONDS) + .atMost(500, TimeUnit.MILLISECONDS) + .until( + () -> ((MaxIntervalTestReconciler) operator.getFirstReconciler()) + .getNumberOfExecutions() > 3); + } + + private MaxIntervalTestCustomResource createTestResource() { + MaxIntervalTestCustomResource cr = new MaxIntervalTestCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName("maxintervaltest1"); + return cr; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java index 9eac198f35..5f86336a1c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java @@ -14,7 +14,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class ObservedGenerationHandlingIT { +class ObservedGenerationHandlingIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -23,7 +23,7 @@ public class ObservedGenerationHandlingIT { .build(); @Test - public void testReconciliationOfNonCustomResourceAndStatusUpdate() { + void testReconciliationOfNonCustomResourceAndStatusUpdate() { var resource = new ObservedGenerationTestCustomResource(); resource.setMetadata(new ObjectMeta()); resource.getMetadata().setName("observed-gen1"); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index 4ab63c7513..c4ab025ac5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class RetryIT { +class RetryIT { public static final int RETRY_INTERVAL = 150; public static final int MAX_RETRY_ATTEMPTS = 5; @@ -36,7 +36,7 @@ public class RetryIT { @Test - public void retryFailedExecution() { + void retryFailedExecution() { RetryTestCustomResource resource = createTestCustomResource("1"); operator.create(RetryTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java index e855b016fd..5ebb05eb0c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java @@ -12,7 +12,7 @@ import static io.javaoperatorsdk.operator.RetryIT.createTestCustomResource; import static org.assertj.core.api.Assertions.assertThat; -public class RetryMaxAttemptIT { +class RetryMaxAttemptIT { public static final int MAX_RETRY_ATTEMPTS = 3; public static final int RETRY_INTERVAL = 100; @@ -31,7 +31,7 @@ public class RetryMaxAttemptIT { @Test - public void retryFailedExecution() throws InterruptedException { + void retryFailedExecution() throws InterruptedException { RetryTestCustomResource resource = createTestCustomResource("max-retry"); operator.create(RetryTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 5998502f32..75597643bf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -18,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class SubResourceUpdateIT { +class SubResourceUpdateIT { @RegisterExtension OperatorExtension operator = @@ -28,7 +28,7 @@ public class SubResourceUpdateIT { .build(); @Test - public void updatesSubResourceStatus() { + void updatesSubResourceStatus() { SubResourceTestCustomResource resource = createTestCustomResource("1"); operator.create(SubResourceTestCustomResource.class, resource); @@ -41,7 +41,7 @@ public void updatesSubResourceStatus() { } @Test - public void updatesSubResourceStatusNoFinalizer() { + void updatesSubResourceStatusNoFinalizer() { SubResourceTestCustomResource resource = createTestCustomResource("1"); resource.getMetadata().setFinalizers(Collections.emptyList()); @@ -57,7 +57,7 @@ public void updatesSubResourceStatusNoFinalizer() { /** Note that we check on controller impl if there is finalizer on execution. */ @Test - public void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { + void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { SubResourceTestCustomResource resource = createTestCustomResource("1"); resource.getMetadata().getFinalizers().clear(); operator.create(SubResourceTestCustomResource.class, resource); @@ -77,7 +77,7 @@ public void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain * fail since its resource version is outdated already. */ @Test - public void updateCustomResourceAfterSubResourceChange() { + void updateCustomResourceAfterSubResourceChange() { SubResourceTestCustomResource resource = createTestCustomResource("1"); operator.create(SubResourceTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index 2a479d47df..6bad954400 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -17,7 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class UpdatingResAndSubResIT { +class UpdatingResAndSubResIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -26,7 +26,7 @@ public class UpdatingResAndSubResIT { .build(); @Test - public void updatesSubResourceStatus() { + void updatesSubResourceStatus() { DoubleUpdateTestCustomResource resource = createTestCustomResource("1"); operator.create(DoubleUpdateTestCustomResource.class, resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResource.java new file mode 100644 index 0000000000..1c6cf81453 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.maxinterval; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("MaxIntervalTestCustomResource") +@ShortNames("mit") +public class MaxIntervalTestCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResourceStatus.java new file mode 100644 index 0000000000..5c403febfb --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestCustomResourceStatus.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.sample.maxinterval; + +public class MaxIntervalTestCustomResourceStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java new file mode 100644 index 0000000000..a5343c27a4 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java @@ -0,0 +1,30 @@ +package io.javaoperatorsdk.operator.sample.maxinterval; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER, + reconciliationMaxInterval = @ReconciliationMaxInterval(interval = 50, + timeUnit = TimeUnit.MILLISECONDS)) +public class MaxIntervalTestReconciler + implements Reconciler, TestExecutionInfoProvider { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + MaxIntervalTestCustomResource resource, Context context) { + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + +} From 071ee05eeb838f8e7eead1d92ae032424eae9f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 31 Jan 2022 15:55:02 +0100 Subject: [PATCH 0267/1608] docs: fix javadoc blocking snapshot release (#889) --- .../operator/api/reconciler/EventSourceInitializer.java | 1 + .../operator/api/reconciler/ReconciliationMaxInterval.java | 6 +++--- .../operator/api/reconciler/UpdateControl.java | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 45f33037d4..79b15380a5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -18,6 +18,7 @@ public interface EventSourceInitializer

{ * * @param context a {@link EventSourceContext} providing access to information useful to event * sources + * @return list of event sources to register */ List prepareEventSources(EventSourceContext

context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java index 05459e7123..b2c1f1c255 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ReconciliationMaxInterval.java @@ -15,13 +15,13 @@ * reconciliation is scheduled with a target interval after the last reconciliation. Note that * this not applies for retries, in case of an exception reconciliation is not scheduled. This is * not a fixed rate, in other words a new reconciliation is scheduled after each reconciliation. - *

+ *

* If an interval is specified by {@link UpdateControl} or {@link DeleteControl}, those take * precedence. - *

+ *

* This is a fail-safe feature, in the sense that if informers are in place and the reconciler * implementation is correct, this feature can be turned off. - *

+ *

* Use NO_RECONCILIATION_MAX_INTERVAL in {@link Constants} to turn off this feature. * * @return max delay between reconciliations diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java index c388958a8a..1762198286 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java @@ -23,6 +23,10 @@ private UpdateControl( * Creates an update control instance that instructs the framework to do an update on resource * itself, not on the status. Note that usually as a results of a reconciliation should be a * status update not an update to the resource itself. + * + * @param custom resource type + * @param customResource customResource to use for update + * @return initialized update control */ public static UpdateControl updateResource(T customResource) { return new UpdateControl<>(customResource, false, true); From 9d3dfd87fba8198a73a3133d63121cfdf867fe67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 31 Jan 2022 18:20:43 +0100 Subject: [PATCH 0268/1608] feat: multiple version in CRD Support (#879) --- .../io/javaoperatorsdk/operator/Operator.java | 3 +- .../operator/ReconcilerUtils.java | 10 ++- .../processing/event/EventSourceManager.java | 2 +- .../operator/ControllerManagerTest.java | 10 +-- ....java => TestCustomReconcilerOtherV1.java} | 4 +- ...V2.java => TestCustomResourceOtherV1.java} | 4 +- .../operator/MultiVersionCRDIT.java | 69 +++++++++++++++++++ .../MultiVersionCRDTestCustomResource1.java | 24 +++++++ .../MultiVersionCRDTestCustomResource2.java | 24 +++++++ ...ultiVersionCRDTestCustomResourceSpec1.java | 26 +++++++ ...ultiVersionCRDTestCustomResourceSpec2.java | 15 ++++ ...tiVersionCRDTestCustomResourceStatus1.java | 40 +++++++++++ ...tiVersionCRDTestCustomResourceStatus2.java | 29 ++++++++ .../MultiVersionCRDTestReconciler1.java | 31 +++++++++ .../MultiVersionCRDTestReconciler2.java | 32 +++++++++ 15 files changed, 309 insertions(+), 14 deletions(-) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/{TestCustomReconcilerV2.java => TestCustomReconcilerOtherV1.java} (69%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/{TestCustomResourceV2.java => TestCustomResourceOtherV1.java} (89%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource1.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource2.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec1.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec2.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus1.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus2.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java 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 39298c2ee0..5f71ec2747 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 @@ -180,7 +180,8 @@ public synchronized void stop() { public synchronized void add(Controller controller) { final var configuration = controller.getConfiguration(); - final var resourceTypeName = configuration.getResourceTypeName(); + final var resourceTypeName = ReconcilerUtils + .getResourceTypeNameWithVersion(configuration.getResourceClass()); final var existing = controllers.get(resourceTypeName); if (existing != null) { throw new OperatorException("Cannot register controller '" + configuration.getName() diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 26dc4cdcca..e3a6da1e5a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -40,9 +40,13 @@ public void setApiVersion(String s) { return Constants.NO_FINALIZER.equals(finalizer) || validator.isFinalizerValid(finalizer); } - public static String getResourceTypeName(Class resourceClass) { - // todo: use fabric8 method when 5.12 is released - // return HasMetadata.getFullResourceName(resourceClass); + public static String getResourceTypeNameWithVersion(Class resourceClass) { + final var version = HasMetadata.getVersion(resourceClass); + return getResourceTypeName(resourceClass) + "/" + version; + } + + public static String getResourceTypeName( + Class resourceClass) { final var group = HasMetadata.getGroup(resourceClass); final var plural = HasMetadata.getPlural(resourceClass); return (group == null || group.isEmpty()) ? plural : plural + "." + group; 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 55f42cd438..22920ac7a2 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 @@ -71,7 +71,7 @@ public void start() { try { eventSource.start(); } catch (Exception e) { - log.warn("Error starting {} -> {}", eventSource, e); + log.warn("Error starting {}", eventSource, e); } } eventProcessor.start(); 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 19f65a9441..c970855f84 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 @@ -10,9 +10,9 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.sample.simple.DuplicateCRController; import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; -import io.javaoperatorsdk.operator.sample.simple.TestCustomReconcilerV2; +import io.javaoperatorsdk.operator.sample.simple.TestCustomReconcilerOtherV1; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; -import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceV2; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResourceOtherV1; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -30,11 +30,11 @@ public void shouldNotAddMultipleControllersForSameCustomResource() { } @Test - public void addingMultipleControllersForCustomResourcesWithDifferentVersionsShouldNotWork() { + public void addingMultipleControllersForCustomResourcesWithSameVersionsShouldNotWork() { final var registered = new TestControllerConfiguration<>(new TestCustomReconciler(null), TestCustomResource.class); - final var duplicated = new TestControllerConfiguration<>(new TestCustomReconcilerV2(), - TestCustomResourceV2.class); + final var duplicated = new TestControllerConfiguration<>(new TestCustomReconcilerOtherV1(), + TestCustomResourceOtherV1.class); checkException(registered, duplicated); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerV2.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java similarity index 69% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerV2.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java index bab22f309a..9a45fbbf93 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerV2.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java @@ -6,10 +6,10 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @ControllerConfiguration -public class TestCustomReconcilerV2 implements Reconciler { +public class TestCustomReconcilerOtherV1 implements Reconciler { @Override - public UpdateControl reconcile(TestCustomResourceV2 resource, + public UpdateControl reconcile(TestCustomResourceOtherV1 resource, Context context) { return UpdateControl.noUpdate(); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceOtherV1.java similarity index 89% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceOtherV1.java index d4c71e0662..f768ba491f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceV2.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceOtherV1.java @@ -6,9 +6,9 @@ import io.fabric8.kubernetes.model.annotation.Version; @Group("sample.javaoperatorsdk.io") -@Version("v2") +@Version("v1") @Kind("TestCustomResource") // this is needed to override the automatically generated kind -public class TestCustomResourceV2 +public class TestCustomResourceOtherV1 extends CustomResource { } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java new file mode 100644 index 0000000000..b417c53e6e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java @@ -0,0 +1,69 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; +import java.util.HashMap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.multiversioncrd.*; + +import static org.awaitility.Awaitility.await; + +class MultiVersionCRDIT { + + public static final String CR_V1_NAME = "crv1"; + public static final String CR_V2_NAME = "crv2"; + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(MultiVersionCRDTestReconciler1.class) + .withReconciler(MultiVersionCRDTestReconciler2.class) + .build(); + + @Test + void multipleCRDVersions() { + operator.create(MultiVersionCRDTestCustomResource1.class, createTestResourceV1WithoutLabel()); + operator.create(MultiVersionCRDTestCustomResource2.class, createTestResourceV2WithLabel()); + + await() + .atMost(Duration.ofSeconds(2)) + .pollInterval(Duration.ofMillis(50)) + .until( + () -> { + var crV1Now = operator.get(MultiVersionCRDTestCustomResource1.class, CR_V1_NAME); + var crV2Now = operator.get(MultiVersionCRDTestCustomResource2.class, CR_V2_NAME); + return crV1Now.getStatus().getReconciledBy().size() == 1 + && crV1Now.getStatus().getReconciledBy() + .contains(MultiVersionCRDTestReconciler1.class.getSimpleName()) + && crV2Now.getStatus().getReconciledBy().size() == 1 + && crV2Now.getStatus().getReconciledBy() + .contains(MultiVersionCRDTestReconciler2.class.getSimpleName()); + }); + } + + MultiVersionCRDTestCustomResource1 createTestResourceV1WithoutLabel() { + MultiVersionCRDTestCustomResource1 cr = new MultiVersionCRDTestCustomResource1(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName(CR_V1_NAME); + cr.setSpec(new MultiVersionCRDTestCustomResourceSpec1()); + cr.getSpec().setValue1(1); + cr.getSpec().setValue2(1); + return cr; + } + + MultiVersionCRDTestCustomResource2 createTestResourceV2WithLabel() { + MultiVersionCRDTestCustomResource2 cr = new MultiVersionCRDTestCustomResource2(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName(CR_V2_NAME); + cr.getMetadata().setLabels(new HashMap<>()); + cr.getMetadata().getLabels().put("version", "v2"); + cr.setSpec(new MultiVersionCRDTestCustomResourceSpec2()); + cr.getSpec().setValue1(1); + return cr; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource1.java new file mode 100644 index 0000000000..10236563c7 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource1.java @@ -0,0 +1,24 @@ +package io.javaoperatorsdk.operator.sample.multiversioncrd; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("MultiVersionCRDTestCustomResource") +@ShortNames("mv1") +public class MultiVersionCRDTestCustomResource1 + extends + CustomResource + implements Namespaced { + + @Override + protected MultiVersionCRDTestCustomResourceStatus1 initStatus() { + return new MultiVersionCRDTestCustomResourceStatus1(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource2.java new file mode 100644 index 0000000000..de0a804049 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResource2.java @@ -0,0 +1,24 @@ +package io.javaoperatorsdk.operator.sample.multiversioncrd; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version(value = "v2", storage = false) +@Kind("MultiVersionCRDTestCustomResource") +@ShortNames("mv2") +public class MultiVersionCRDTestCustomResource2 + extends + CustomResource + implements Namespaced { + + @Override + protected MultiVersionCRDTestCustomResourceStatus2 initStatus() { + return new MultiVersionCRDTestCustomResourceStatus2(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec1.java new file mode 100644 index 0000000000..20759eef76 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec1.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.sample.multiversioncrd; + +public class MultiVersionCRDTestCustomResourceSpec1 { + + private int value1; + + private int value2; + + public int getValue1() { + return value1; + } + + public MultiVersionCRDTestCustomResourceSpec1 setValue1(int value1) { + this.value1 = value1; + return this; + } + + public int getValue2() { + return value2; + } + + public MultiVersionCRDTestCustomResourceSpec1 setValue2(int value2) { + this.value2 = value2; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec2.java new file mode 100644 index 0000000000..e97d110673 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec2.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.multiversioncrd; + +public class MultiVersionCRDTestCustomResourceSpec2 { + + private int value1; + + public int getValue1() { + return value1; + } + + public MultiVersionCRDTestCustomResourceSpec2 setValue1(int value1) { + this.value1 = value1; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus1.java new file mode 100644 index 0000000000..535292c33f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus1.java @@ -0,0 +1,40 @@ +package io.javaoperatorsdk.operator.sample.multiversioncrd; + +import java.util.ArrayList; +import java.util.List; + +public class MultiVersionCRDTestCustomResourceStatus1 { + + private int value1; + + private int value2; + + private List reconciledBy = new ArrayList<>(); + + public int getValue1() { + return value1; + } + + public MultiVersionCRDTestCustomResourceStatus1 setValue1(int value1) { + this.value1 = value1; + return this; + } + + public int getValue2() { + return value2; + } + + public MultiVersionCRDTestCustomResourceStatus1 setValue2(int value2) { + this.value2 = value2; + return this; + } + + public List getReconciledBy() { + return reconciledBy; + } + + public MultiVersionCRDTestCustomResourceStatus1 setReconciledBy(List reconciledBy) { + this.reconciledBy = reconciledBy; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus2.java new file mode 100644 index 0000000000..5df57ef76d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus2.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.sample.multiversioncrd; + +import java.util.ArrayList; +import java.util.List; + +public class MultiVersionCRDTestCustomResourceStatus2 { + + private int value1; + + private List reconciledBy = new ArrayList<>(); + + public int getValue1() { + return value1; + } + + public MultiVersionCRDTestCustomResourceStatus2 setValue1(int value1) { + this.value1 = value1; + return this; + } + + public List getReconciledBy() { + return reconciledBy; + } + + public MultiVersionCRDTestCustomResourceStatus2 setReconciledBy(List reconciledBy) { + this.reconciledBy = reconciledBy; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java new file mode 100644 index 0000000000..a8f58971b3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java @@ -0,0 +1,31 @@ +package io.javaoperatorsdk.operator.sample.multiversioncrd; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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 static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER, labelSelector = "!version") +public class MultiVersionCRDTestReconciler1 + implements Reconciler { + + private static final Logger log = LoggerFactory.getLogger(MultiVersionCRDTestReconciler1.class); + + @Override + public UpdateControl reconcile( + MultiVersionCRDTestCustomResource1 resource, Context context) { + log.info("Reconcile MultiVersionCRDTestCustomResource1: {}", + resource.getMetadata().getName()); + resource.getStatus().setValue1(resource.getStatus().getValue1() + 1); + resource.getStatus().setValue2(resource.getStatus().getValue2() + 1); + if (!resource.getStatus().getReconciledBy().contains(getClass().getSimpleName())) { + resource.getStatus().getReconciledBy().add(getClass().getSimpleName()); + } + return UpdateControl.updateStatus(resource); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java new file mode 100644 index 0000000000..d25297d1c6 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java @@ -0,0 +1,32 @@ +package io.javaoperatorsdk.operator.sample.multiversioncrd; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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 static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration( + finalizerName = NO_FINALIZER, + labelSelector = "version in (v2)") +public class MultiVersionCRDTestReconciler2 + implements Reconciler { + + private static final Logger log = LoggerFactory.getLogger(MultiVersionCRDTestReconciler2.class); + + @Override + public UpdateControl reconcile( + MultiVersionCRDTestCustomResource2 resource, Context context) { + log.info("Reconcile MultiVersionCRDTestCustomResource2: {}", + resource.getMetadata().getName()); + resource.getStatus().setValue1(resource.getStatus().getValue1() + 1); + if (!resource.getStatus().getReconciledBy().contains(getClass().getSimpleName())) { + resource.getStatus().getReconciledBy().add(getClass().getSimpleName()); + } + return UpdateControl.updateStatus(resource); + } +} From eeea472d487762a5cde340433dcac1e4c7201ae2 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 31 Jan 2022 18:21:31 +0100 Subject: [PATCH 0269/1608] fix: also run full tests on `next` branch pushes and PRs (#884) --- .github/workflows/e2e-test-mysql.yml | 3 ++- .github/workflows/e2e-test-tomcat.yml | 3 ++- .github/workflows/pr.yml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-test-mysql.yml b/.github/workflows/e2e-test-mysql.yml index 07fe6e3189..91edea16cb 100644 --- a/.github/workflows/e2e-test-mysql.yml +++ b/.github/workflows/e2e-test-mysql.yml @@ -3,10 +3,11 @@ name: MySQL Schema Operator End to End test on: pull_request: - branches: [ main, v1 ] + branches: [ main, v1, next ] push: branches: - main + - next jobs: mysql_e2e_test: runs-on: ubuntu-latest diff --git a/.github/workflows/e2e-test-tomcat.yml b/.github/workflows/e2e-test-tomcat.yml index 70e01660d0..4f5e5e820b 100644 --- a/.github/workflows/e2e-test-tomcat.yml +++ b/.github/workflows/e2e-test-tomcat.yml @@ -3,10 +3,11 @@ name: Tomcat Operator End to End test on: pull_request: - branches: [ main, v1 ] + branches: [ main, v1, next ] push: branches: - main + - next jobs: tomcat_e2e_test: runs-on: ubuntu-latest diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f46bd258f7..f21457e61d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,7 +8,7 @@ concurrency: cancel-in-progress: true on: pull_request: - branches: [ main, v1 ] + branches: [ main, v1, next ] workflow_dispatch: jobs: build: From 45b187dc302ccfea098215266a80b494c3942954 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 31 Jan 2022 22:21:49 +0100 Subject: [PATCH 0270/1608] fix: decouple DependentResource creation from configuration (#883) The idea is to delay instantiation until needed and rely on DependentResourceControllerFactory for this purpose instead. Not sure if we actually need to use an interface there or not. --- .../api/config/ControllerConfiguration.java | 2 +- .../DefaultControllerConfiguration.java | 6 +- .../config/DefaultResourceConfiguration.java | 3 +- .../DependentResourceConfiguration.java | 10 ++ .../DependentResourceController.java | 11 ++- .../DependentResourceControllerFactory.java | 24 +++-- ...ernetesDependentResourceConfiguration.java | 65 +++++++++---- ...KubernetesDependentResourceController.java | 10 +- .../dependent/DependentResourceManager.java | 9 +- .../informer/InformerConfiguration.java | 92 +++++++++++-------- .../runtime/AnnotationConfiguration.java | 55 +++++------ 11 files changed, 181 insertions(+), 106 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java 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 d4739f8267..7e44a45de9 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 @@ -48,7 +48,7 @@ default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } - default List getDependentResources() { + default List getDependentResources() { return Collections.emptyList(); } 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 index 0a3c68c70b..25ff5a5315 100644 --- 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 @@ -18,7 +18,7 @@ public class DefaultControllerConfiguration private final boolean generationAware; private final RetryConfiguration retryConfiguration; private final ResourceEventFilter resourceEventFilter; - private final List dependents; + private final List dependents; // NOSONAR constructor is meant to provide all information public DefaultControllerConfiguration( @@ -33,7 +33,7 @@ public DefaultControllerConfiguration( ResourceEventFilter resourceEventFilter, Class resourceClass, ConfigurationService service, - List dependents) { + List dependents) { super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -96,7 +96,7 @@ public ResourceEventFilter getEventFilter() { } @Override - public List getDependentResources() { + public List getDependentResources() { return dependents; } } 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 7b80416433..d7b5f76815 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 @@ -23,8 +23,7 @@ public DefaultResourceConfiguration(String labelSelector, Class resourceClass public DefaultResourceConfiguration(String labelSelector, Class resourceClass, Set namespaces) { this.labelSelector = labelSelector; - this.resourceClass = resourceClass == null ? ResourceConfiguration.super.getResourceClass() - : resourceClass; + this.resourceClass = resourceClass; this.namespaces = namespaces != null ? namespaces : Collections.emptySet(); this.watchAllNamespaces = this.namespaces.isEmpty(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java new file mode 100644 index 0000000000..15911e0418 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface DependentResourceConfiguration { + + Class> getDependentResourceClass(); + + Class getResourceClass(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java index 043aec2f53..21cd16a32f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java @@ -2,11 +2,12 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -public class DependentResourceController +public class DependentResourceController> implements DependentResource, Builder, Updater, Persister, Cleaner { @@ -15,14 +16,16 @@ public class DependentResourceController private final Cleaner cleaner; private final Persister persister; private final DependentResource delegate; + private final C configuration; @SuppressWarnings("unchecked") - public DependentResourceController(DependentResource delegate) { + public DependentResourceController(DependentResource delegate, C configuration) { this.delegate = delegate; builder = (delegate instanceof Builder) ? (Builder) delegate : null; updater = (delegate instanceof Updater) ? (Updater) delegate : null; cleaner = (delegate instanceof Cleaner) ? (Cleaner) delegate : null; persister = initPersister(delegate); + this.configuration = configuration; } @SuppressWarnings("unchecked") @@ -84,4 +87,8 @@ public void createOrReplace(R dependentResource, Context context) { public R getFor(P primary, Context context) { return persister.getFor(primary, context); } + + public C getConfiguration() { + return configuration; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java index f814cd7a1a..dd11a7a706 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java @@ -1,14 +1,26 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import java.lang.reflect.InvocationTargetException; + import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; public interface DependentResourceControllerFactory

{ - default DependentResourceController from(DependentResource dependent) { - // todo: this needs to be cleaned-up / redesigned - return dependent instanceof DependentResourceController - ? (DependentResourceController) dependent - : new DependentResourceController<>(dependent); + default , R, C extends DependentResourceConfiguration> T from( + C config) { + try { + final var dependentResource = config.getDependentResourceClass().getConstructor() + .newInstance(); + if (config instanceof KubernetesDependentResourceConfiguration) { + return (T) new KubernetesDependentResourceController(dependentResource, + (KubernetesDependentResourceConfiguration) config); + } else { + return (T) new DependentResourceController(dependentResource, config); + } + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException + | IllegalAccessException e) { + throw new IllegalArgumentException(e); + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java index dad9411d2d..23d37881b4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java @@ -4,35 +4,64 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; -public class KubernetesDependentResourceConfiguration - extends InformerConfiguration { +public interface KubernetesDependentResourceConfiguration + extends InformerConfiguration, DependentResourceConfiguration { - private final boolean owned; + class DefaultKubernetesDependentResourceConfiguration + extends DefaultInformerConfiguration + implements KubernetesDependentResourceConfiguration { - protected KubernetesDependentResourceConfiguration( - ConfigurationService service, - String labelSelector, Class resourceClass, - PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange, Set namespaces, boolean owned) { - super(service, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, - skipUpdateEventPropagationIfNoChange, namespaces); - this.owned = owned; + private final boolean owned; + private final Class> dependentResourceClass; + + protected DefaultKubernetesDependentResourceConfiguration( + ConfigurationService service, + String labelSelector, Class resourceClass, + PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange, Set namespaces, boolean owned, + Class> dependentResourceClass) { + super(service, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, + skipUpdateEventPropagationIfNoChange, namespaces); + this.owned = owned; + this.dependentResourceClass = dependentResourceClass; + } + + public boolean isOwned() { + return owned; + } + + @Override + public Class> getDependentResourceClass() { + return dependentResourceClass; + } + + @Override + public Class getResourceClass() { + return super.getResourceClass(); + } } - public static KubernetesDependentResourceConfiguration from( - InformerConfiguration cfg, boolean owned) { - return new KubernetesDependentResourceConfiguration<>(cfg.getConfigurationService(), + static KubernetesDependentResourceConfiguration from( + InformerConfiguration cfg, boolean owned, + Class dependentResourceClass) { + return new DefaultKubernetesDependentResourceConfiguration(cfg.getConfigurationService(), cfg.getLabelSelector(), cfg.getResourceClass(), cfg.getPrimaryResourcesRetriever(), cfg.getAssociatedResourceIdentifier(), cfg.isSkipUpdateEventPropagationIfNoChange(), - cfg.getNamespaces(), owned); + cfg.getNamespaces(), owned, + (Class>) dependentResourceClass); } - public boolean isOwned() { - return owned; + boolean isOwned(); + + @Override + default Class getResourceClass() { + return InformerConfiguration.super.getResourceClass(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java index 015b60f4bb..172e9cb0d9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java @@ -12,7 +12,8 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; public class KubernetesDependentResourceController - extends DependentResourceController { + extends DependentResourceController> { + private final KubernetesDependentResourceConfiguration configuration; private KubernetesClient client; private InformerEventSource informer; @@ -20,7 +21,7 @@ public class KubernetesDependentResourceController delegate, KubernetesDependentResourceConfiguration configuration) { - super(delegate); + super(delegate, configuration); // todo: check if we can validate that types actually match properly final var associatedPrimaries = (delegate instanceof PrimaryResourcesRetriever) @@ -36,7 +37,8 @@ public KubernetesDependentResourceController(DependentResource delegate, .withAssociatedSecondaryResourceIdentifier(associatedSecondary) .build(); this.configuration = - KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned()); + KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned(), + configuration.getDependentResourceClass()); } @Override @@ -69,6 +71,6 @@ public R getFor(P primary, Context context) { } public boolean owned() { - return configuration.isOwned(); + return getConfiguration().isOwned(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 30e92e3d63..911805b396 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -8,7 +8,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -42,13 +42,14 @@ public DependentResourceManager(Controller controller) { @Override public List prepareEventSources(EventSourceContext context) { - final List configured = configuration.getDependentResources(); + final List configured = configuration.getDependentResources(); dependents = new ArrayList<>(configured.size()); List sources = new ArrayList<>(configured.size() + 5); configured.forEach(dependent -> { - dependents.add(configuration.dependentFactory().from(dependent)); - sources.add(dependent.initEventSource(context)); + final var dependentResourceController = configuration.dependentFactory().from(dependent); + dependents.add(dependentResourceController); + sources.add(dependentResourceController.initEventSource(context)); }); return sources; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java index 0c06698327..49b7ae7654 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java @@ -7,45 +7,57 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -public class InformerConfiguration - extends DefaultResourceConfiguration { - - private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; - private final AssociatedSecondaryResourceIdentifier

associatedWith; - private final boolean skipUpdateEventPropagationIfNoChange; - - protected InformerConfiguration(ConfigurationService service, String labelSelector, - Class resourceClass, - PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange, Set namespaces) { - super(labelSelector, resourceClass, namespaces); - setConfigurationService(service); - this.secondaryToPrimaryResourcesIdSet = - Objects.requireNonNullElse(secondaryToPrimaryResourcesIdSet, Mappers.fromOwnerReference()); - this.associatedWith = - Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); - this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; - } +public interface InformerConfiguration + extends ResourceConfiguration { + + class DefaultInformerConfiguration extends + DefaultResourceConfiguration implements InformerConfiguration { + + private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; + private final AssociatedSecondaryResourceIdentifier

associatedWith; + private final boolean skipUpdateEventPropagationIfNoChange; + + protected DefaultInformerConfiguration(ConfigurationService service, String labelSelector, + Class resourceClass, + PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, + AssociatedSecondaryResourceIdentifier

associatedWith, + boolean skipUpdateEventPropagationIfNoChange, Set namespaces) { + super(labelSelector, resourceClass, namespaces); + setConfigurationService(service); + this.secondaryToPrimaryResourcesIdSet = + Objects.requireNonNullElse(secondaryToPrimaryResourcesIdSet, + Mappers.fromOwnerReference()); + this.associatedWith = + Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); + this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; + } - public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { - return secondaryToPrimaryResourcesIdSet; - } + public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { + return secondaryToPrimaryResourcesIdSet; + } - public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier() { - return associatedWith; - } + public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier() { + return associatedWith; + } - public boolean isSkipUpdateEventPropagationIfNoChange() { - return skipUpdateEventPropagationIfNoChange; + public boolean isSkipUpdateEventPropagationIfNoChange() { + return skipUpdateEventPropagationIfNoChange; + } } - public static class InformerConfigurationBuilder { + PrimaryResourcesRetriever getPrimaryResourcesRetriever(); + + AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier(); + + boolean isSkipUpdateEventPropagationIfNoChange(); + + class InformerConfigurationBuilder { private PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; private AssociatedSecondaryResourceIdentifier

associatedWith; @@ -101,32 +113,32 @@ public InformerConfigurationBuilder withLabelSelector(String labelSelector } public InformerConfiguration build() { - return new InformerConfiguration<>(configurationService, labelSelector, resourceClass, + return new DefaultInformerConfiguration<>(configurationService, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, skipUpdateEventPropagationIfNoChange, namespaces); } } - public static InformerConfigurationBuilder from( + static InformerConfigurationBuilder from( EventSourceContext

context, Class resourceClass) { return new InformerConfigurationBuilder<>(resourceClass, context.getConfigurationService()); } - public static InformerConfigurationBuilder from(ConfigurationService configurationService, + static InformerConfigurationBuilder from(ConfigurationService configurationService, Class resourceClass) { return new InformerConfigurationBuilder<>(resourceClass, configurationService); } - public static InformerConfigurationBuilder from( + static InformerConfigurationBuilder from( InformerConfiguration configuration) { return new InformerConfigurationBuilder(configuration.getResourceClass(), configuration.getConfigurationService()) - .withNamespaces(configuration.getNamespaces()) - .withLabelSelector(configuration.getLabelSelector()) - .skippingEventPropagationIfUnchanged( - configuration.isSkipUpdateEventPropagationIfNoChange()) - .withAssociatedSecondaryResourceIdentifier( - configuration.getAssociatedResourceIdentifier()) - .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); + .withNamespaces(configuration.getNamespaces()) + .withLabelSelector(configuration.getLabelSelector()) + .skippingEventPropagationIfUnchanged( + configuration.isSkipUpdateEventPropagationIfNoChange()) + .withAssociatedSecondaryResourceIdentifier( + configuration.getAssociatedResourceIdentifier()) + .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 30d2600cfe..1ffde6de36 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.config.runtime; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -12,11 +11,11 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Dependent; import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.config.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; 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.informer.InformerConfiguration; @@ -28,7 +27,7 @@ public class AnnotationConfiguration private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; - private List dependents; + private List dependentConfigurations; public AnnotationConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -121,27 +120,20 @@ public ResourceEventFilter getEventFilter() { } @Override - public List getDependentResources() { - if (dependents == null) { - final var dependentConfigs = valueOrDefault(annotation, - ControllerConfiguration::dependents, new Dependent[] {}); - if (dependentConfigs.length > 0) { - dependents = new ArrayList<>(dependentConfigs.length); - for (Dependent dependentConfig : dependentConfigs) { - final Class dependentType = dependentConfig.type(); - DependentResource dependent; - try { - dependent = dependentType.getConstructor().newInstance(); - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException - | IllegalAccessException e) { - throw new IllegalArgumentException(e); - } + public List getDependentResources() { + if (dependentConfigurations == null) { + final var dependents = valueOrDefault(annotation, ControllerConfiguration::dependents, + new Dependent[]{}); + if (dependents.length > 0) { + dependentConfigurations = new ArrayList<>(dependents.length); + for (Dependent dependent : dependents) { + final Class dependentType = dependent.type(); + final var resourceType = dependent.resourceType(); - final var resourceType = dependentConfig.resourceType(); if (HasMetadata.class.isAssignableFrom(resourceType)) { final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); final var namespaces = - valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[] {}); + valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[]{}); final var labelSelector = valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, @@ -154,17 +146,28 @@ public List getDependentResources() { .skippingEventPropagationIfUnchanged(skipIfUnchanged) .withNamespaces(namespaces) .build(); - dependent = new KubernetesDependentResourceController(dependent, - KubernetesDependentResourceConfiguration.from(configuration, owned)); - } - dependents.add(dependent); + dependentConfigurations.add( + KubernetesDependentResourceConfiguration.from(configuration, owned, dependentType)); + } else { + dependentConfigurations.add(new DependentResourceConfiguration() { + @Override + public Class getDependentResourceClass() { + return dependentType; + } + + @Override + public Class getResourceClass() { + return resourceType; + } + }); + } } } else { - dependents = Collections.emptyList(); + dependentConfigurations = Collections.emptyList(); } } - return dependents; + return dependentConfigurations; } private static T valueOrDefault(C annotation, Function mapper, T defaultValue) { From 489e25d29274950d3fe0ae358f74ce27bd7fd3e5 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Tue, 1 Feb 2022 15:42:32 +0000 Subject: [PATCH 0271/1608] Disable Sonar check on forks (#896) --- .github/workflows/sonar.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index df359ea008..8e24bc49b9 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -15,6 +15,7 @@ on: jobs: test: runs-on: ubuntu-latest + if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.repository_owner == 'java-operator-sdk' ) }} steps: - uses: actions/checkout@v2 - name: Set up Java and Maven From a6e961c11632f1e94f55cb1d8d0fe9fd0394fad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Feb 2022 16:44:26 +0100 Subject: [PATCH 0272/1608] fix: operator exception on miss of informer (#894) --- .../event/source/controller/ControllerResourceCache.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java index 25c08d6af6..ddc7a7658f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceCache.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.Cloner; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.Cache; @@ -52,6 +53,11 @@ public Optional get(ResourceID resourceID) { sharedIndexInformer = sharedIndexInformers.get(resourceID.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)); } + if (sharedIndexInformer == null) { + throw new OperatorException( + "Cannot find informer for ResourceID: " + resourceID + ". This is usually " + + "due to invalid resource id mapping for registered informers."); + } var resource = sharedIndexInformer.getStore() .getByKey(io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( resourceID.getNamespace().orElse(null), From 14403fb1908773afe439835406970c2f1bcf5a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Feb 2022 17:02:05 +0100 Subject: [PATCH 0273/1608] feat: incompatible crd versions in test (#895) --- .../operator/MultiVersionCRDIT.java | 33 +++++++++++++++++-- ...ultiVersionCRDTestCustomResourceSpec1.java | 20 +++-------- ...ultiVersionCRDTestCustomResourceSpec2.java | 10 +++--- ...tiVersionCRDTestCustomResourceStatus1.java | 11 ------- .../MultiVersionCRDTestReconciler1.java | 1 - 5 files changed, 40 insertions(+), 35 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java index b417c53e6e..930fc210b3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java @@ -11,12 +11,14 @@ import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.multiversioncrd.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.awaitility.Awaitility.await; class MultiVersionCRDIT { public static final String CR_V1_NAME = "crv1"; public static final String CR_V2_NAME = "crv2"; + @RegisterExtension OperatorExtension operator = OperatorExtension.builder() @@ -46,13 +48,38 @@ void multipleCRDVersions() { }); } + @Test + void invalidEventsDoesNotBreakEventHandling() { + var v2res = createTestResourceV2WithLabel(); + v2res.getMetadata().getLabels().clear(); + operator.create(MultiVersionCRDTestCustomResource2.class, v2res); + var v1res = createTestResourceV1WithoutLabel(); + operator.create(MultiVersionCRDTestCustomResource1.class, v1res); + + await() + .atMost(Duration.ofSeconds(2)) + .pollInterval(Duration.ofMillis(50)) + .until(() -> { + var crV1Now = operator.get(MultiVersionCRDTestCustomResource1.class, CR_V1_NAME); + return crV1Now.getStatus().getReconciledBy() + .contains(MultiVersionCRDTestReconciler1.class.getSimpleName()); + }); + assertThat( + operator + .get(MultiVersionCRDTestCustomResource2.class, CR_V2_NAME) + .getStatus() + .getReconciledBy() + .size()) + .isZero(); + } + + MultiVersionCRDTestCustomResource1 createTestResourceV1WithoutLabel() { MultiVersionCRDTestCustomResource1 cr = new MultiVersionCRDTestCustomResource1(); cr.setMetadata(new ObjectMeta()); cr.getMetadata().setName(CR_V1_NAME); cr.setSpec(new MultiVersionCRDTestCustomResourceSpec1()); - cr.getSpec().setValue1(1); - cr.getSpec().setValue2(1); + cr.getSpec().setValue(1); return cr; } @@ -63,7 +90,7 @@ MultiVersionCRDTestCustomResource2 createTestResourceV2WithLabel() { cr.getMetadata().setLabels(new HashMap<>()); cr.getMetadata().getLabels().put("version", "v2"); cr.setSpec(new MultiVersionCRDTestCustomResourceSpec2()); - cr.getSpec().setValue1(1); + cr.getSpec().setValue("string value"); return cr; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec1.java index 20759eef76..5c915179e2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec1.java @@ -2,25 +2,15 @@ public class MultiVersionCRDTestCustomResourceSpec1 { - private int value1; + private int value; - private int value2; - - public int getValue1() { - return value1; + public int getValue() { + return value; } - public MultiVersionCRDTestCustomResourceSpec1 setValue1(int value1) { - this.value1 = value1; + public MultiVersionCRDTestCustomResourceSpec1 setValue(int value) { + this.value = value; return this; } - public int getValue2() { - return value2; - } - - public MultiVersionCRDTestCustomResourceSpec1 setValue2(int value2) { - this.value2 = value2; - return this; - } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec2.java index e97d110673..a4058dbd9f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceSpec2.java @@ -2,14 +2,14 @@ public class MultiVersionCRDTestCustomResourceSpec2 { - private int value1; + private String value; - public int getValue1() { - return value1; + public String getValue() { + return value; } - public MultiVersionCRDTestCustomResourceSpec2 setValue1(int value1) { - this.value1 = value1; + public MultiVersionCRDTestCustomResourceSpec2 setValue(String value) { + this.value = value; return this; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus1.java index 535292c33f..765766b1e2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestCustomResourceStatus1.java @@ -7,8 +7,6 @@ public class MultiVersionCRDTestCustomResourceStatus1 { private int value1; - private int value2; - private List reconciledBy = new ArrayList<>(); public int getValue1() { @@ -20,15 +18,6 @@ public MultiVersionCRDTestCustomResourceStatus1 setValue1(int value1) { return this; } - public int getValue2() { - return value2; - } - - public MultiVersionCRDTestCustomResourceStatus1 setValue2(int value2) { - this.value2 = value2; - return this; - } - public List getReconciledBy() { return reconciledBy; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java index a8f58971b3..ab8ad417be 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java @@ -22,7 +22,6 @@ public UpdateControl reconcile( log.info("Reconcile MultiVersionCRDTestCustomResource1: {}", resource.getMetadata().getName()); resource.getStatus().setValue1(resource.getStatus().getValue1() + 1); - resource.getStatus().setValue2(resource.getStatus().getValue2() + 1); if (!resource.getStatus().getReconciledBy().contains(getClass().getSimpleName())) { resource.getStatus().getReconciledBy().add(getClass().getSimpleName()); } From 7b2189f560bfaa53ed8308cdce4aa408a2c6c532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 2 Feb 2022 15:51:57 +0100 Subject: [PATCH 0274/1608] feat: remove feature of filter event with observed generation (#902) --- docs/documentation/features.md | 28 +++++------ .../operator/api/ObservedGenerationAware.java | 4 +- .../controller/ResourceEventFilters.java | 12 ----- .../event/source/ResourceEventFilterTest.java | 48 ------------------- 4 files changed, 14 insertions(+), 78 deletions(-) diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 9b83bb4348..95924e24dd 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -137,17 +137,13 @@ mostly for the cases when there is a long waiting period after a delete operatio you might want to either schedule a timed event to make sure `cleanup` is executed again or use event sources to get notified about the state changes of a deleted resource. -## Generation Awareness and Automatic Observed Generation Handling +## Automatic Observed Generation Handling Having `.observedGeneration` value on the status of the resource is a best practice to indicate the last generation of the resource reconciled successfully by the controller. This helps the users / administrators to check if the custom -resource was reconciled, but it is used to decide if a reconciliation should happen or not. Filtering events based on -generation is supported by the framework and turned on by default. There are two modes. +resource was reconciled. -### Primary (preferred) Mode - -The first and the **preferred** one is to check after a resource event received, if the generation of the resource is -larger than the `.observedGeneration` field on status. In order to have this feature working: +In order to have this feature working: - the **status class** (not the resource) must implement the [`ObservedGenerationAware`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java) @@ -189,16 +185,18 @@ public class WebPage extends CustomResource } ``` -### The Second (Fallback) Mode +## Generation Awareness and Event Filtering + +On an operator startup, the best practice is to reconcile all the resources. Since while operator was down, changes +might have made both to custom resource and dependent resources. -The second, fallback mode is (when the conditions from above are not met to handle the observed generation automatically -in status) to handle generation filtering in memory. Thus, if an event is received, the generation of the received -resource is compared with the resource in the cache. +When the first reconciliation is done successfully, the next reconciliation is triggered if either the dependent +resources are changed or the custom resource `.spec` is changed. If other fields like `.metadata` is changed on the +custom resource, the reconciliation could be skipped. This is supported out of the box, thus the reconciliation by +default is not triggered if the change to the main custom resource does not increase the `.metadata.generation` field. +Note that the increase of `.metada.generation` is handled automatically by Kubernetes. -Note that the **first approach has significant benefits** in the situation when the operator is restarted and there is -no cached resource anymore. In case two this leads to a reconciliation of every resource in all cases, -event if the resource is not changed while the operator was not running. However, in case informers are used -the reconciliation from startup will happen anyway, since events will be propagated by the informer. +To turn on this feature set `generationAwareEventProcessing` to `false` for the `Reconciler`. ## Support for Well Known (non-custom) Kubernetes Resources diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java index 54b1321d52..43927b8136 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -6,9 +6,7 @@ /** * If the custom resource's status implements this interface, the observed generation will be - * automatically handled. The last observed generation will be updated on status. In addition to - * that, controller configuration will be checked if is set to be generation aware. If generation - * aware config is turned off, this interface is ignored. + * automatically handled. The last observed generation will be updated on status. *

* In order for this automatic handling to work the status object returned by * {@link CustomResource#getStatus()} should not be null. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java index d287361c2b..c314af7f2e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java @@ -1,8 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.ObservedGenerationAware; /** * Convenience implementations of, and utility methods for, {@link ResourceEventFilter}. @@ -25,16 +23,6 @@ public final class ResourceEventFilters { private static final ResourceEventFilter GENERATION_AWARE = (configuration, oldResource, newResource) -> { final var generationAware = configuration.isGenerationAware(); - if (newResource instanceof CustomResource) { - var newCustomResource = (CustomResource) newResource; - final var status = newCustomResource.getStatus(); - if (generationAware && status instanceof ObservedGenerationAware) { - var actualGeneration = newResource.getMetadata().getGeneration(); - var observedGeneration = ((ObservedGenerationAware) status) - .getObservedGeneration(); - return observedGeneration == null || actualGeneration > observedGeneration; - } - } return oldResource == null || !generationAware || oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); }; 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 433614fe17..1dd9972dea 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,7 +8,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; @@ -20,7 +19,6 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; -import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.Mockito.any; @@ -101,27 +99,6 @@ public void eventFilteredByCustomPredicateAndGenerationAware() { verify(eventHandler, times(1)).handleEvent(any()); } - @Test - public void observedGenerationFiltering() { - var config = new ObservedGenControllerConfig(FINALIZER, true, null); - - var eventSource = init(new ObservedGenController(config)); - - ObservedGenCustomResource cr = new ObservedGenCustomResource(); - cr.setMetadata(new ObjectMeta()); - cr.getMetadata().setFinalizers(List.of(FINALIZER)); - cr.getMetadata().setGeneration(5L); - cr.getStatus().setObservedGeneration(5L); - - eventSource.eventReceived(ResourceAction.UPDATED, cr, null); - verify(eventHandler, times(0)).handleEvent(any()); - - cr.getMetadata().setGeneration(6L); - - eventSource.eventReceived(ResourceAction.UPDATED, cr, null); - verify(eventHandler, times(1)).handleEvent(any()); - } - @Test public void eventAlwaysFilteredByCustomPredicate() { var config = new TestControllerConfig( @@ -147,13 +124,6 @@ public TestControllerConfig(String finalizer, boolean generationAware, super(finalizer, generationAware, eventFilter, TestCustomResource.class); } } - private static class ObservedGenControllerConfig - extends ControllerConfig { - public ObservedGenControllerConfig(String finalizer, boolean generationAware, - ResourceEventFilter eventFilter) { - super(finalizer, generationAware, eventFilter, ObservedGenCustomResource.class); - } - } private static class ControllerConfig extends DefaultControllerConfiguration { @@ -192,22 +162,4 @@ public MixedOperation { - - public ObservedGenController( - ControllerConfiguration configuration) { - super(null, configuration, null); - } - - @Override - public EventSourceManager getEventSourceManager() { - return mock(EventSourceManager.class); - } - - @Override - public MixedOperation, Resource> getCRClient() { - return mock(MixedOperation.class); - } - } } From 28c05b9d4be41ae5a075dae7ee71933664d7cf81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20CROCQUESEL?= <88554524+scrocquesel@users.noreply.github.com> Date: Wed, 2 Feb 2022 16:02:16 +0100 Subject: [PATCH 0275/1608] test(mysql-schema): fix sql query in userExists (#898) --- .../javaoperatorsdk/operator/sample/schema/SchemaService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java index 8c6cd31b70..2944a88fd8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/SchemaService.java @@ -76,7 +76,7 @@ public static void deleteSchemaAndRelatedUser(Connection connection, String sche private static boolean userExists(Connection connection, String username) { try (PreparedStatement ps = connection.prepareStatement( - "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = ?)")) { + "SELECT 1 FROM mysql.user WHERE user = ?")) { ps.setString(1, username); try (ResultSet resultSet = ps.executeQuery()) { return resultSet.next(); From 376bc6814f9797ad6d1623d957f1a446c0d272c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 2 Feb 2022 16:04:08 +0100 Subject: [PATCH 0276/1608] fix: sonar run on PR from own repo (#903) --- .github/workflows/sonar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 8e24bc49b9..05c7d6f753 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -15,7 +15,7 @@ on: jobs: test: runs-on: ubuntu-latest - if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.repository_owner == 'java-operator-sdk' ) }} + if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'java-operator-sdk' ) }} steps: - uses: actions/checkout@v2 - name: Set up Java and Maven From e0f7b100734709f4f2b5c38e777ac6518eda3b5a Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 2 Feb 2022 16:08:33 +0100 Subject: [PATCH 0277/1608] fix: fail explicitly if current namespace is requested but not available (#900) Fixes #897 --- .../operator/processing/Controller.java | 34 +++++++++---------- .../runtime/AnnotationConfiguration.java | 2 -- 2 files changed, 16 insertions(+), 20 deletions(-) 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 5e93b0d7af..f9a600f1a7 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 @@ -1,7 +1,6 @@ package io.javaoperatorsdk.operator.processing; import java.util.List; -import java.util.Objects; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; @@ -28,6 +27,7 @@ import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +@SuppressWarnings({"unchecked"}) public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { private final Reconciler reconciler; @@ -165,6 +165,10 @@ public void start() throws OperatorException { final String controllerName = configuration.getName(); final var crdName = configuration.getResourceTypeName(); final var specVersion = "v1"; + + // fail early if we're missing the current namespace information + failOnMissingCurrentNS(); + try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config @@ -188,12 +192,7 @@ public void start() throws OperatorException { configurationService(), kubernetesClient)) .forEach(eventSourceManager::registerEventSource); } - if (failOnMissingCurrentNS()) { - throw new OperatorException( - "Controller '" - + controllerName - + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); - } + eventSourceManager.start(); } catch (MissingCRDException e) { throwMissingCRDException(crdName, specVersion, controllerName); @@ -231,19 +230,18 @@ private void throwMissingCRDException(String crdName, String specVersion, String } /** - * Determines whether we should fail because the current namespace is request as target namespace - * but is missing - * - * @return {@code true} if the current namespace is requested but is missing, {@code false} - * otherwise + * Throws an {@link OperatorException} if the controller is configured to watch the current + * namespace but it's absent from the configuration. */ - private boolean failOnMissingCurrentNS() { - if (configuration.watchCurrentNamespace()) { - final var effectiveNamespaces = configuration.getEffectiveNamespaces(); - return effectiveNamespaces.size() == 1 - && effectiveNamespaces.stream().allMatch(Objects::isNull); + private void failOnMissingCurrentNS() { + try { + configuration.getEffectiveNamespaces(); + } catch (OperatorException e) { + throw new OperatorException( + "Controller '" + + configuration.getName() + + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } - return false; } public EventSourceManager getEventSourceManager() { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 8977bf3ff8..a1cf34a010 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -110,8 +110,6 @@ public ResourceEventFilter getEventFilter() { : ResourceEventFilters.passthrough(); } - - @Override public Optional reconciliationMaxInterval() { if (annotation.reconciliationMaxInterval() != null) { From f0892699aac4f847c272f615fdb350aa43d5d701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 2 Feb 2022 16:10:29 +0100 Subject: [PATCH 0278/1608] fix: polling event source improvements (#901) --- .../source/polling/PollingEventSource.java | 44 +++++++++++++++---- .../polling/PollingEventSourceTest.java | 10 +++-- 2 files changed, 41 insertions(+), 13 deletions(-) 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 8701f31182..55609c2918 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 @@ -11,6 +11,35 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; +/** + *

+ * Pols resource (on contrary to {@link PerResourcePollingEventSource}) not per resource bases but + * instead to calls supplier periodically and independently of the number of state of custom + * resources managed by the operator. It is called on start (synced). This means that when the + * reconciler first time executed on startup a poll already happened before. So if the cache does + * not contain the target resource it means it is not created yet or was deleted while an operator + * was not running. + *

+ *

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

+ * So the generic workflow in reconciler should be: + * + *
    + *
  • Check if the cache contains the resource.
  • + *
  • If cache contains the resource reconcile it - compare with target state, update if necessary + *
  • + *
  • if cache not contains the resource create it.
  • + *
  • If the resource was created or updated, put the new version of the resource manually to the + * cache.
  • + *
+ * + * @param type of the polled resource + * @param

primary resource type + */ public class PollingEventSource extends CachingEventSource { private static final Logger log = LoggerFactory.getLogger(PollingEventSource.class); @@ -29,6 +58,7 @@ public PollingEventSource(Supplier> supplier, @Override public void start() throws OperatorException { super.start(); + getStateAndFillCache(); timer.schedule(new TimerTask() { @Override public void run() { @@ -47,6 +77,10 @@ protected void getStateAndFillCache() { cache.keys().filter(e -> !values.containsKey(e)).forEach(super::handleDelete); } + public void put(ResourceID key, T resource) { + cache.put(key, resource); + } + @Override public void stop() throws OperatorException { super.stop(); @@ -61,15 +95,7 @@ public void stop() throws OperatorException { */ @Override public Optional getAssociated(P primary) { - return getValueFromCacheOrSupplier(ResourceID.fromResource(primary)); + return getCachedValue(ResourceID.fromResource(primary)); } - public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { - var resource = getCachedValue(resourceID); - if (resource.isPresent()) { - return resource; - } - getStateAndFillCache(); - return getCachedValue(resourceID); - } } 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 db1ea74aab..d866791147 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 @@ -21,16 +21,18 @@ class PollingEventSourceTest AbstractEventSourceTestBase, EventHandler> { private Supplier> supplier = mock(Supplier.class); + private PollingEventSource pollingEventSource = + new PollingEventSource<>(supplier, 50, SampleExternalResource.class); @BeforeEach public void setup() { - setUpSource(new PollingEventSource<>(supplier, 50, SampleExternalResource.class)); + setUpSource(pollingEventSource, false); } @Test public void pollsAndProcessesEvents() throws InterruptedException { when(supplier.get()).thenReturn(testResponseWithTwoValues()); - + pollingEventSource.start(); Thread.sleep(100); verify(eventHandler, times(2)).handleEvent(any()); @@ -40,7 +42,7 @@ public void pollsAndProcessesEvents() throws InterruptedException { public void propagatesEventForRemovedResources() throws InterruptedException { when(supplier.get()).thenReturn(testResponseWithTwoValues()) .thenReturn(testResponseWithOneValue()); - + pollingEventSource.start(); Thread.sleep(150); verify(eventHandler, times(3)).handleEvent(any()); @@ -49,7 +51,7 @@ public void propagatesEventForRemovedResources() throws InterruptedException { @Test public void doesNotPropagateEventIfResourceNotChanged() throws InterruptedException { when(supplier.get()).thenReturn(testResponseWithTwoValues()); - + pollingEventSource.start(); Thread.sleep(250); verify(eventHandler, times(2)).handleEvent(any()); From cdef02ff4b40a532937d0caabe568250ad367caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 2 Feb 2022 16:20:58 +0100 Subject: [PATCH 0279/1608] fix: javadoc blocking snapshot release (#904) --- .../processing/event/source/polling/PollingEventSource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 55609c2918..4702bd8c38 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 @@ -13,7 +13,7 @@ /** *

- * Pols resource (on contrary to {@link PerResourcePollingEventSource}) not per resource bases but + * Polls resource (on contrary to {@link PerResourcePollingEventSource}) not per resource bases but * instead to calls supplier periodically and independently of the number of state of custom * resources managed by the operator. It is called on start (synced). This means that when the * reconciler first time executed on startup a poll already happened before. So if the cache does @@ -24,7 +24,7 @@ * Another caveat with this is if the cached object is checked in the reconciler and created since * not in the cache it should be manually added to the cache, since it can happen that the * reconciler is triggered before the cache is propagated with the new resource from a scheduled - * execution. See {@link PollingEventSource##put(ResourceID, Object)}. + * execution. See {@link #put(ResourceID, Object)} method. *

* So the generic workflow in reconciler should be: * From dcc5f277452fa23eea943d7a4c89be9cfa5ca2e3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 2 Feb 2022 15:27:15 +0000 Subject: [PATCH 0280/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 9ca97c7186..8e5f9ab481 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index a615428790..04ed9af26c 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 69a163b7d7..38acf8c29e 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index fd124ea222..25477d0d2c 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index a520155deb..f7aaa1b89e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 58d1f3155a..f38aa9bc17 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 9877aaef0d..3b3c74f283 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index ffcd7038e7..9ab41d0e57 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 90522733af..827f6d9b40 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index db2926c74b..e65be3aefb 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 3dcdb530c7..6f8e9a2d65 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 87ccbbf48d..0a16d9112b 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index b652916960..c839a902a3 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.0.3-SNAPSHOT + 2.1.1-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 42a7e65fb837d62cf00d577f9c1c7c612ecd7a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 3 Feb 2022 18:05:45 +0100 Subject: [PATCH 0281/1608] Docs best practices improvements and link fixes (#875) --- docs/_includes/hero.html | 6 +-- docs/documentation/patterns-best-practices.md | 48 +++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/_includes/hero.html b/docs/_includes/hero.html index 100fca649c..d0057b772c 100644 --- a/docs/_includes/hero.html +++ b/docs/_includes/hero.html @@ -5,13 +5,13 @@ {% endif %}

[ {{ site.title }} ]

diff --git a/docs/documentation/patterns-best-practices.md b/docs/documentation/patterns-best-practices.md index 519045f5d3..14416a595a 100644 --- a/docs/documentation/patterns-best-practices.md +++ b/docs/documentation/patterns-best-practices.md @@ -7,17 +7,59 @@ permalink: /docs/patterns-best-practices # Patterns and Best Practices -This document describes patters and best practices, to build and run operators, and how to implement them in terms -of Java Operator SDK. +This document describes patterns and best practices, to build and run operators, and how to implement them in terms of +Java Operator SDK. + +See also best practices in [Operator SDK](https://sdk.operatorframework.io/docs/best-practices/best-practices/). ## Implementing a Reconciler +### Reconcile All The Resources All the Time + +The reconciliation can be triggered by events from multiple sources. It could be tempting to check the events and +reconcile just the related resource or subset of resources that the controller manages. However, this is **considered as an +anti-pattern** in operators. If triggered, all resources should be reconciled. Usually this means only +comparing the target state with the current state in the cache for most of the resource. +The reason behind this is events not reliable in generally, this means events can be lost. In addition to that operator +can crash and while down will miss events. + +In addition to that such approach might even complicate implementation logic in the `Reconciler`, since parallel +execution of the reconciler is not allowed for the same custom resource, there can be multiple events received for the +same resource or dependent resource during an ongoing execution, ordering those events could be also challenging. + +Since there is a consensus regarding this in the industry, from v2 the events are not even accessible for +the `Reconciler`. + ### Idempotency -### Sync of Async Way of Resource Handling +Since all the resources are reconciled during an execution and an execution can be triggered quite often, also +retries of a reconciliation can happen naturally in operators, the implementation of a `Reconciler` +needs to be idempotent. Luckily, since operators are usually managing already declarative resources, this is trivial +to do in most cases. + +### Sync or Async Way of Resource Handling + +In an implementation of reconciliation there can be a point when reconciler needs to wait a non-insignificant amount +of time while a resource gets up and running. For example, reconciler would do some additional step only if a Pod is ready +to receive requests. This problem can be approached in two ways synchronously or asynchronously. + +The async way is just return from the reconciler, if there are informers properly in place for the target resource, +reconciliation will be triggered on change. During the reconciliation the pod can be read from the cache of the informer +and a check on it's state can be conducted again. The benefit of this approach is that it will free up the thread, +so it can be used to reconcile other resources. + +The sync way would be to periodically poll the cache of the informer for the pod's state, until the target state +is reached. This would block the thread until the state is reached, which in some cases could take quite long. ## Why to Have Automated Retries? +Automatic retries are in place by default, it can be fine-tuned, but in general it's not advised to turn +of automatic retries. One of the reason is that, issues like network error naturally happens, and are usually +solved by a retry. Another typical situation is for example when a dependent resource or the custom resource is updated, +during the update usually there is optimistic version control in place. So if someone updated the resource during +reconciliation, maybe using `kubectl` or another process, the update would fail on a conflict. A retry solves this +problem simply by executing the reconciliation again. + ## Managing State ## Dependent Resources From eeed3237936ebced5a53f0e65140afb01659a53c Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 4 Feb 2022 09:43:02 +0100 Subject: [PATCH 0282/1608] fix: NPE when no namespace is provided in kube config (#899) * fix: fail explicitly if current namespace is requested but not available Fixes #897 * fix: retain annotation for reflective access --- .../api/config/KubernetesDependent.java | 3 ++ .../api/config/ResourceConfiguration.java | 8 ++++- .../operator/processing/Controller.java | 35 ++++++++----------- .../informer/InformerConfiguration.java | 14 ++++---- .../runtime/AnnotationConfiguration.java | 4 +-- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java index 587fd58f85..5b9fbd38f3 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java @@ -1,10 +1,13 @@ package io.javaoperatorsdk.operator.api.config; import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; +@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface KubernetesDependent { 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 1e1d58b917..3ebc3ae546 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 @@ -4,6 +4,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Constants; @@ -67,7 +68,12 @@ default Set getEffectiveNamespaces() { throw new IllegalStateException( "Parent ConfigurationService must be set before calling this method"); } - targetNamespaces = Collections.singleton(parent.getClientConfiguration().getNamespace()); + String namespace = parent.getClientConfiguration().getNamespace(); + if (namespace == null) { + throw new OperatorException( + "Couldn't retrieve the currently connected namespace. Make sure it's correctly set in your ~/.kube/config file, using, e.g. 'kubectl config set-context --namespace='"); + } + targetNamespaces = Collections.singleton(namespace); } return targetNamespaces; } 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 be819c5621..f3ad2dd8ec 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 @@ -2,7 +2,6 @@ import java.util.LinkedList; import java.util.List; -import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,7 +33,7 @@ import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -@SuppressWarnings({"rawtypes", "unchecked"}) +@SuppressWarnings({"unchecked"}) @Ignore public class Controller implements Reconciler, LifecycleAware, EventSourceInitializer { @@ -195,6 +194,10 @@ public void start() throws OperatorException { final var specVersion = "v1"; log.info("Starting '{}' controller for reconciler: {}, resource: {}", controllerName, reconciler.getClass().getCanonicalName(), resClass.getCanonicalName()); + + // fail early if we're missing the current namespace information + failOnMissingCurrentNS(); + try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config @@ -210,13 +213,6 @@ public void start() throws OperatorException { CustomResourceUtils.assertCustomResource(resClass, crd); } - if (failOnMissingCurrentNS()) { - throw new OperatorException( - "Controller '" - + controllerName - + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); - } - final var context = new EventSourceContext<>( eventSourceManager.getControllerResourceEventSource().getResourceCache(), configurationService(), kubernetesClient); @@ -263,19 +259,18 @@ private void throwMissingCRDException(String crdName, String specVersion, String } /** - * Determines whether we should fail because the current namespace is request as target namespace - * but is missing - * - * @return {@code true} if the current namespace is requested but is missing, {@code false} - * otherwise + * Throws an {@link OperatorException} if the controller is configured to watch the current + * namespace but it's absent from the configuration. */ - private boolean failOnMissingCurrentNS() { - if (configuration.watchCurrentNamespace()) { - final var effectiveNamespaces = configuration.getEffectiveNamespaces(); - return effectiveNamespaces.size() == 1 - && effectiveNamespaces.stream().allMatch(Objects::isNull); + private void failOnMissingCurrentNS() { + try { + configuration.getEffectiveNamespaces(); + } catch (OperatorException e) { + throw new OperatorException( + "Controller '" + + configuration.getName() + + "' is configured to watch the current namespace but it couldn't be inferred from the current configuration."); } - return false; } public EventSourceManager getEventSourceManager() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java index 49b7ae7654..087e206f8b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java @@ -133,12 +133,12 @@ static InformerConfigurationBuild InformerConfiguration configuration) { return new InformerConfigurationBuilder(configuration.getResourceClass(), configuration.getConfigurationService()) - .withNamespaces(configuration.getNamespaces()) - .withLabelSelector(configuration.getLabelSelector()) - .skippingEventPropagationIfUnchanged( - configuration.isSkipUpdateEventPropagationIfNoChange()) - .withAssociatedSecondaryResourceIdentifier( - configuration.getAssociatedResourceIdentifier()) - .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); + .withNamespaces(configuration.getNamespaces()) + .withLabelSelector(configuration.getLabelSelector()) + .skippingEventPropagationIfUnchanged( + configuration.isSkipUpdateEventPropagationIfNoChange()) + .withAssociatedSecondaryResourceIdentifier( + configuration.getAssociatedResourceIdentifier()) + .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 1ffde6de36..3e0721b8a0 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -123,7 +123,7 @@ public ResourceEventFilter getEventFilter() { public List getDependentResources() { if (dependentConfigurations == null) { final var dependents = valueOrDefault(annotation, ControllerConfiguration::dependents, - new Dependent[]{}); + new Dependent[] {}); if (dependents.length > 0) { dependentConfigurations = new ArrayList<>(dependents.length); for (Dependent dependent : dependents) { @@ -133,7 +133,7 @@ public List getDependentResources() { if (HasMetadata.class.isAssignableFrom(resourceType)) { final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); final var namespaces = - valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[]{}); + valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[] {}); final var labelSelector = valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, From 9c07b6df5f5a0e70b4d49a4f8c804345e704d4a1 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Fri, 4 Feb 2022 11:57:18 +0000 Subject: [PATCH 0283/1608] chore(ci): reduce the combinations in CI (#908) --- .github/workflows/pr.yml | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f46bd258f7..f6c5935cd0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -11,20 +11,15 @@ on: branches: [ main, v1 ] workflow_dispatch: jobs: - build: + check_format_and_unit_tests: runs-on: ubuntu-latest - strategy: - matrix: - java: [ 11, 17 ] - distribution: [ temurin ] - kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1', 'v1.23.0' ] steps: - uses: actions/checkout@v2 - name: Set up Java and Maven uses: actions/setup-java@v2 with: - distribution: ${{ matrix.distribution }} - java-version: ${{ matrix.java }} + distribution: temurin + java-version: 17 cache: 'maven' - name: Check code format run: | @@ -32,6 +27,32 @@ jobs: ./mvnw ${MAVEN_ARGS} impsort:check --file pom.xml - name: Run unit tests run: ./mvnw ${MAVEN_ARGS} -B test --file pom.xml + + integration_tests: + runs-on: ubuntu-latest + strategy: + matrix: + java: [ 11, 17 ] + kubernetes: [ 'v1.17.13','v1.18.20','v1.19.14','v1.20.10','v1.21.4', 'v1.22.1', 'v1.23.0' ] + exclude: + - java: 11 + kubernetes: 'v1.18.20' + - java: 11 + kubernetes: 'v1.19.14' + - java: 11 + kubernetes: 'v1.20.10' + - java: 11 + kubernetes: 'v1.21.4' + - java: 11 + kubernetes: 'v1.22.1' + steps: + - uses: actions/checkout@v2 + - name: Set up Java and Maven + uses: actions/setup-java@v2 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: 'maven' - name: Set up Minikube uses: manusa/actions-setup-minikube@v2.4.3 with: From 6f8d38d73b4a60c35b602994a2cf0f3ee6a70634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 4 Feb 2022 13:31:25 +0100 Subject: [PATCH 0284/1608] docs: best practices (#907) --- docs/documentation/patterns-best-practices.md | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/docs/documentation/patterns-best-practices.md b/docs/documentation/patterns-best-practices.md index 14416a595a..20dafcf30d 100644 --- a/docs/documentation/patterns-best-practices.md +++ b/docs/documentation/patterns-best-practices.md @@ -17,54 +17,69 @@ See also best practices in [Operator SDK](https://sdk.operatorframework.io/docs/ ### Reconcile All The Resources All the Time The reconciliation can be triggered by events from multiple sources. It could be tempting to check the events and -reconcile just the related resource or subset of resources that the controller manages. However, this is **considered as an -anti-pattern** in operators. If triggered, all resources should be reconciled. Usually this means only -comparing the target state with the current state in the cache for most of the resource. -The reason behind this is events not reliable in generally, this means events can be lost. In addition to that operator -can crash and while down will miss events. +reconcile just the related resource or subset of resources that the controller manages. However, this is **considered as +an anti-pattern** in operators. If triggered, all resources should be reconciled. Usually this means only comparing the +target state with the current state in the cache for most of the resource. The reason behind this is events not reliable +In general, this means events can be lost. In addition to that the operator can crash and while down will miss events. In addition to that such approach might even complicate implementation logic in the `Reconciler`, since parallel execution of the reconciler is not allowed for the same custom resource, there can be multiple events received for the same resource or dependent resource during an ongoing execution, ordering those events could be also challenging. -Since there is a consensus regarding this in the industry, from v2 the events are not even accessible for +Since there is a consensus regarding this in the industry, from v2 the events are not even accessible for the `Reconciler`. +### EventSources and Caching + +As mentioned above during a reconciliation best practice is to reconcile all the dependent resources managed by the +controller. This means that we want to compare a target state with the actual state of the cluster. Reading the actual +state of a resource from the Kubernetes API Server directly all the time would mean a significant load. Therefore, it's +a common practice to instead create a watch for the dependent resources and cache their latest state. This is done +following the Informer pattern. In Java Operator SDK, informer is wrapped into an EventSource, to integrate it with the +eventing system of the framework, resulting in `InformerEventSource`. + +A new event that triggers the reconciliation is propagated when the actual resource is already in cache. So in +reconciler what should be just done is to compare the target calculated state of a dependent resource of the actual +state from the cache of the event source. If it is changed or not in the cache it needs to be created, respectively +updated. + ### Idempotency -Since all the resources are reconciled during an execution and an execution can be triggered quite often, also -retries of a reconciliation can happen naturally in operators, the implementation of a `Reconciler` -needs to be idempotent. Luckily, since operators are usually managing already declarative resources, this is trivial -to do in most cases. +Since all the resources are reconciled during an execution and an execution can be triggered quite often, also retries +of a reconciliation can happen naturally in operators, the implementation of a `Reconciler` +needs to be idempotent. Luckily, since operators are usually managing already declarative resources, this is trivial to +do in most cases. ### Sync or Async Way of Resource Handling -In an implementation of reconciliation there can be a point when reconciler needs to wait a non-insignificant amount -of time while a resource gets up and running. For example, reconciler would do some additional step only if a Pod is ready -to receive requests. This problem can be approached in two ways synchronously or asynchronously. +In an implementation of reconciliation there can be a point when reconciler needs to wait a non-insignificant amount of +time while a resource gets up and running. For example, reconciler would do some additional step only if a Pod is ready +to receive requests. This problem can be approached in two ways synchronously or asynchronously. -The async way is just return from the reconciler, if there are informers properly in place for the target resource, +The async way is just return from the reconciler, if there are informers properly in place for the target resource, reconciliation will be triggered on change. During the reconciliation the pod can be read from the cache of the informer -and a check on it's state can be conducted again. The benefit of this approach is that it will free up the thread, -so it can be used to reconcile other resources. +and a check on it's state can be conducted again. The benefit of this approach is that it will free up the thread, so it +can be used to reconcile other resources. -The sync way would be to periodically poll the cache of the informer for the pod's state, until the target state -is reached. This would block the thread until the state is reached, which in some cases could take quite long. +The sync way would be to periodically poll the cache of the informer for the pod's state, until the target state is +reached. This would block the thread until the state is reached, which in some cases could take quite long. ## Why to Have Automated Retries? -Automatic retries are in place by default, it can be fine-tuned, but in general it's not advised to turn -of automatic retries. One of the reason is that, issues like network error naturally happens, and are usually -solved by a retry. Another typical situation is for example when a dependent resource or the custom resource is updated, -during the update usually there is optimistic version control in place. So if someone updated the resource during -reconciliation, maybe using `kubectl` or another process, the update would fail on a conflict. A retry solves this -problem simply by executing the reconciliation again. +Automatic retries are in place by default, it can be fine-tuned, but in general it's not advised to turn of automatic +retries. One of the reasons is that issues like network error naturally happen and are usually solved by a retry. +Another typical situation is for example when a dependent resource or the custom resource is updated, during the update +usually there is optimistic version control in place. So if someone updated the resource during reconciliation, maybe +using `kubectl` or another process, the update would fail on a conflict. A retry solves this problem simply by executing +the reconciliation again. ## Managing State -## Dependent Resources - -### EventSources and Caching - -### Why are Events Irrelevant? +When managing only kubernetes resources an explicit state is not necessary about the resources. The state can be +read/watched, also filtered using labels. Or just following some naming convention. However, when managing external +resources, there can be a situation for example when the created resource can only be addressed by an ID generated when +the resource was created. This ID needs to be stored, so on next reconciliation it could be used to addressing the +resource. One place where it could go is the status sub-resource. On the other hand by definition status should be just +the result of a reconciliation. Therefore, it's advised in general, to put such state into a separate resource usually a +Kubernetes Secret or ConfigMap or a dedicated CustomResource, where the structure can be also validated. From b282d4e5783247e055a7c2a3630fd1062953e274 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 4 Feb 2022 13:38:38 +0100 Subject: [PATCH 0285/1608] fix: version update to 2.2.0-SNAPSHOT --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 73c8e7ed7d..11f70ef4ca 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index a1804086d6..79ccc716df 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 6f66b97810..9f8f4e5c01 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 8e7ff3fe8f..a624af4e23 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index fbca5b3ae2..6d7905118c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 64be615e03..c8932f902f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index e5819f32a8..666e471240 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 5c75031b16..cee6935745 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 94e4dbcb83..566ec8b314 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT webpage diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 96cd79f960..6dcdded08b 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 02aae34d54..ffbe7c659b 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 0b26c7de42..21b8162183 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index c41e3c1cfc..51860f7af8 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.0-SNAPSHOT + 2.2.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From b5098c429c421a2e5ddedca760540b96f3e08917 Mon Sep 17 00:00:00 2001 From: csviri Date: Fri, 4 Feb 2022 14:10:15 +0100 Subject: [PATCH 0286/1608] fix: merge issues from master --- .../operator/api/config/ControllerConfiguration.java | 2 -- .../operator/api/config/DefaultControllerConfiguration.java | 5 +---- .../io/javaoperatorsdk/operator/ControllerManagerTest.java | 2 +- .../processing/event/source/ResourceEventFilterTest.java | 6 +----- .../controller/ControllerResourceEventSourceTest.java | 2 +- .../operator/config/runtime/AnnotationConfiguration.java | 2 +- 6 files changed, 5 insertions(+), 14 deletions(-) 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 51218effa0..717e339514 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 @@ -1,11 +1,9 @@ package io.javaoperatorsdk.operator.api.config; -import java.lang.reflect.ParameterizedType; 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.ReconcilerUtils; 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 index 82db2a4af7..41bacbbbce 100644 --- 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 @@ -2,8 +2,8 @@ import java.time.Duration; import java.util.Collections; -import java.util.Optional; import java.util.List; +import java.util.Optional; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -44,10 +44,7 @@ public DefaultControllerConfiguration( this.crdName = crdName; this.finalizer = finalizer; this.generationAware = generationAware; - this.namespaces = - namespaces != null ? Collections.unmodifiableSet(namespaces) : Collections.emptySet(); this.reconciliationMaxInterval = reconciliationMaxInterval; - this.watchAllNamespaces = this.namespaces.isEmpty(); this.retryConfiguration = retryConfiguration == null ? ControllerConfiguration.super.getRetryConfiguration() 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 ca02094d46..910492a47f 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 @@ -63,7 +63,7 @@ private static class TestControllerConfiguration 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); this.controller = controller; } 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 734a89434e..0dfb2d09d7 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 @@ -7,11 +7,7 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.ObjectMeta; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; -import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; @@ -144,7 +140,7 @@ public ControllerConfig(String finalizer, boolean generationAware, eventFilter, customResourceClass, null, - null); + null, null); } } 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 f67ea44a5d..83528256df 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 @@ -163,7 +163,7 @@ public TestConfiguration(boolean generationAware) { null, TestCustomResource.class, null, - null); + null, null); } } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index d8b9e99dae..5d10762f35 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -1,9 +1,9 @@ package io.javaoperatorsdk.operator.config.runtime; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.time.Duration; import java.util.Optional; import java.util.Set; import java.util.function.Function; From ff1a44dc032cf1bc3342af9572f9c6b778bb0d90 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Fri, 4 Feb 2022 14:06:38 +0000 Subject: [PATCH 0287/1608] feat: enable easy E2E & improvements to the integration test suite (#891) --- .github/workflows/e2e-test-mysql.yml | 83 -------- .github/workflows/e2e-test-tomcat.yml | 79 ------- .github/workflows/e2e-test.yml | 50 +++++ .../junit/AbstractOperatorExtension.java | 176 +++++++++++++++ .../operator/junit/E2EOperatorExtension.java | 200 ++++++++++++++++++ .../operator/junit/OperatorExtension.java | 143 ++++--------- sample-operators/mysql-schema/pom.xml | 6 + .../sample/MySQLSchemaReconciler.java | 2 +- .../sample/MySQLSchemaOperatorE2E.java | 125 +++++++---- sample-operators/tomcat-operator/pom.xml | 6 + .../operator/sample/WebappReconciler.java | 4 +- .../operator/sample/TomcatOperatorE2E.java | 114 +++++----- 12 files changed, 638 insertions(+), 350 deletions(-) delete mode 100644 .github/workflows/e2e-test-mysql.yml delete mode 100644 .github/workflows/e2e-test-tomcat.yml create mode 100644 .github/workflows/e2e-test.yml create mode 100644 operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java create mode 100644 operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java diff --git a/.github/workflows/e2e-test-mysql.yml b/.github/workflows/e2e-test-mysql.yml deleted file mode 100644 index 07fe6e3189..0000000000 --- a/.github/workflows/e2e-test-mysql.yml +++ /dev/null @@ -1,83 +0,0 @@ -# End to end integration test which deploys the Tomcat operator to a Kubernetes -# (Kind) cluster and creates custom resources to verify the operator's functionality -name: MySQL Schema Operator End to End test -on: - pull_request: - branches: [ main, v1 ] - push: - branches: - - main -jobs: - mysql_e2e_test: - runs-on: ubuntu-latest - env: - KIND_CL_NAME: e2e-test - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: clean resident local docker - if: ${{ env.ACT }} - continue-on-error: true - run: | - for DIMG in "$KIND_CL_NAME-control-plane "; do - docker stop $DIMG ; docker rm $DIMG ; - done ; - sleep 1 - - - name: Create Kubernetes KinD Cluster - uses: container-tools/kind-action@v1.7.0 - with: - cluster_name: e2e-test - registry: false - - - name: Deploy MySQL DB - working-directory: sample-operators/mysql-schema - run: | - kubectl create namespace mysql - kubectl apply -f k8s/mysql-deployment.yaml - kubectl apply -f k8s/mysql-service.yaml - - - name: Set up Java and Maven - uses: actions/setup-java@v2 - with: - java-version: 11 - distribution: adopt-hotspot - cache: 'maven' - - - name: Build SDK - run: mvn install -DskipTests - - - name: build jib - working-directory: sample-operators/mysql-schema - run: | - mvn --version - mvn -B package jib:dockerBuild jib:buildTar -Djib-maven-image=mysql-schema-operator -DskipTests - kind load image-archive target/jib-image.tar --name=${{ env.KIND_CL_NAME }} - - - name: Apply CRDs - working-directory: sample-operators/mysql-schema - run: | - kubectl apply -f target/classes/META-INF/fabric8/mysqlschemas.mysql.sample.javaoperatorsdk-v1.yml - - - name: Deploy MySQL Operator - working-directory: sample-operators/mysql-schema - run: | - kubectl apply -f k8s/operator.yaml - - - name: Run E2E Tests - working-directory: sample-operators/mysql-schema - run: mvn -B test -P end-to-end-tests - - - name: Dump state - if: ${{ failure() }} - run: | - set +e - echo "All namespaces" - kubectl get ns - echo "All objects in mysql" - kubectl get all -n mysql-schema-test" -o yaml - echo "Output of mysql pod" - kubectl logs -l app=mysql-schema-operator -n mysql-schema - echo "All objects in mysql-schema-test" - kubectl get deployment,pod,tomcat,webapp -n mysql-schema-test" -o yaml diff --git a/.github/workflows/e2e-test-tomcat.yml b/.github/workflows/e2e-test-tomcat.yml deleted file mode 100644 index 70e01660d0..0000000000 --- a/.github/workflows/e2e-test-tomcat.yml +++ /dev/null @@ -1,79 +0,0 @@ -# End to end integration test which deploys the Tomcat operator to a Kubernetes -# (Kind) cluster and creates custom resources to verify the operator's functionality -name: Tomcat Operator End to End test -on: - pull_request: - branches: [ main, v1 ] - push: - branches: - - main -jobs: - tomcat_e2e_test: - runs-on: ubuntu-latest - env: - KIND_CL_NAME: e2e-test - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: clean resident local docker - if: ${{ env.ACT }} - continue-on-error: true - run: | - for DIMG in "$KIND_CL_NAME-control-plane "; do - docker stop $DIMG ; docker rm $DIMG ; - done ; - sleep 1 - - - name: Create Kubernetes KinD Cluster - uses: container-tools/kind-action@v1.7.0 - with: - cluster_name: e2e-test - registry: false - - - name: Set up Java and Maven - uses: actions/setup-java@v2 - with: - java-version: 11 - distribution: adopt-hotspot - cache: 'maven' - - - name: Build SDK - run: mvn install -DskipTests - - - name: build jib - working-directory: sample-operators/tomcat-operator - run: | - mvn --version - mvn -B package jib:dockerBuild jib:buildTar -Djib-maven-image=tomcat-operator -DskipTests - kind load image-archive target/jib-image.tar --name=${{ env.KIND_CL_NAME }} - - - name: Apply CRDs - working-directory: sample-operators/tomcat-operator - run: | - kubectl apply -f target/classes/META-INF/fabric8/tomcats.tomcatoperator.io-v1.yml - kubectl apply -f target/classes/META-INF/fabric8/webapps.tomcatoperator.io-v1.yml - - - name: Deploy Tomcat Operator - working-directory: sample-operators/tomcat-operator - run: | - kubectl apply -f k8s/operator.yaml - - - name: Run E2E Tests - working-directory: sample-operators/tomcat-operator - run: mvn -B test -P end-to-end-tests - - - name: Dump state - if: ${{ failure() }} - run: | - set +e - echo "All namespaces" - kubectl get ns - echo "All objects in tomcat-operator" - kubectl get all -n tomcat-operator -o yaml - echo "Output of tomcat-operator pod" - kubectl logs -l app=tomcat-operator -n tomcat-operator - echo "All objects in tomcat-test" - kubectl get deployment,pod,tomcat,webapp -n tomcat-test -o yaml - echo "Output of curl command" - kubectl logs curl -n tomcat-test diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 0000000000..31b89e1b20 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,50 @@ +# Integration and end to end tests which runs locally and deploys the Operator to a Kubernetes +# (Minikube) cluster and creates custom resources to verify the operator's functionality +name: Integration & End to End tests +on: + pull_request: + branches: [ main ] + push: + branches: + - main + +jobs: + sample_operators_tests: + strategy: + matrix: + sample_dir: + - "sample-operators/mysql-schema" + - "sample-operators/tomcat-operator" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Minikube-Kubernetes + uses: manusa/actions-setup-minikube@v2.4.3 + with: + minikube version: v1.24.0 + kubernetes version: v1.23.0 + github token: ${{ secrets.GITHUB_TOKEN }} + driver: docker + + - name: Set up Java and Maven + uses: actions/setup-java@v2 + with: + java-version: 17 + distribution: temurin + cache: 'maven' + + - name: Build SDK + run: mvn install -DskipTests + + - name: Run integration tests in local mode + working-directory: ${{ matrix.sample_dir }} + run: | + mvn test -P end-to-end-tests + + - name: Run E2E tests as a deployment + working-directory: ${{ matrix.sample_dir }} + run: | + eval $(minikube -p minikube docker-env) + mvn jib:dockerBuild test -P end-to-end-tests -Dtest.deployment=remote diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java new file mode 100644 index 0000000000..fb204e8c57 --- /dev/null +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -0,0 +1,176 @@ +package io.javaoperatorsdk.operator.junit; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +import org.junit.jupiter.api.extension.*; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.fabric8.kubernetes.client.utils.Utils; +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.Version; + +public abstract class AbstractOperatorExtension implements HasKubernetesClient, + BeforeAllCallback, + BeforeEachCallback, + AfterAllCallback, + AfterEachCallback { + + protected final KubernetesClient kubernetesClient; + protected final ConfigurationService configurationService; + protected final List infrastructure; + protected Duration infrastructureTimeout; + protected final boolean oneNamespacePerClass; + protected final boolean preserveNamespaceOnError; + protected final boolean waitForNamespaceDeletion; + + protected String namespace; + + protected AbstractOperatorExtension( + ConfigurationService configurationService, + List infrastructure, + Duration infrastructureTimeout, + boolean oneNamespacePerClass, + boolean preserveNamespaceOnError, + boolean waitForNamespaceDeletion) { + + this.kubernetesClient = new DefaultKubernetesClient(); + this.configurationService = configurationService; + this.infrastructure = infrastructure; + this.infrastructureTimeout = infrastructureTimeout; + this.oneNamespacePerClass = oneNamespacePerClass; + this.preserveNamespaceOnError = preserveNamespaceOnError; + this.waitForNamespaceDeletion = waitForNamespaceDeletion; + } + + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + beforeAllImpl(context); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + beforeEachImpl(context); + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { + afterAllImpl(context); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + afterEachImpl(context); + } + + @Override + public KubernetesClient getKubernetesClient() { + return kubernetesClient; + } + + public String getNamespace() { + return namespace; + } + + public NonNamespaceOperation, Resource> resources( + Class type) { + return kubernetesClient.resources(type).inNamespace(namespace); + } + + public T get(Class type, String name) { + return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); + } + + public T create(Class type, T resource) { + return kubernetesClient.resources(type).inNamespace(namespace).create(resource); + } + + public T replace(Class type, T resource) { + return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); + } + + public boolean delete(Class type, T resource) { + return kubernetesClient.resources(type).inNamespace(namespace).delete(resource); + } + + protected void beforeAllImpl(ExtensionContext context) { + if (oneNamespacePerClass) { + namespace = context.getRequiredTestClass().getSimpleName(); + namespace += "-"; + namespace += UUID.randomUUID(); + namespace = KubernetesResourceUtil.sanitizeName(namespace).toLowerCase(Locale.US); + namespace = namespace.substring(0, Math.min(namespace.length(), 63)); + + before(context); + } + } + + protected void beforeEachImpl(ExtensionContext context) { + if (!oneNamespacePerClass) { + namespace = context.getRequiredTestClass().getSimpleName(); + namespace += "-"; + namespace += context.getRequiredTestMethod().getName(); + namespace += "-"; + namespace += UUID.randomUUID(); + namespace = KubernetesResourceUtil.sanitizeName(namespace).toLowerCase(Locale.US); + namespace = namespace.substring(0, Math.min(namespace.length(), 63)); + + before(context); + } + } + + protected abstract void before(ExtensionContext context); + + protected void afterAllImpl(ExtensionContext context) { + if (oneNamespacePerClass) { + after(context); + } + } + + protected void afterEachImpl(ExtensionContext context) { + if (!oneNamespacePerClass) { + after(context); + } + } + + protected abstract void after(ExtensionContext context); + + public static abstract class AbstractBuilder { + protected ConfigurationService configurationService; + protected final List infrastructure; + protected Duration infrastructureTimeout; + protected boolean preserveNamespaceOnError; + protected boolean waitForNamespaceDeletion; + protected boolean oneNamespacePerClass; + + protected AbstractBuilder() { + this.configurationService = new BaseConfigurationService(Version.UNKNOWN); + + this.infrastructure = new ArrayList<>(); + this.infrastructureTimeout = Duration.ofMinutes(1); + + this.preserveNamespaceOnError = Utils.getSystemPropertyOrEnvVar( + "josdk.it.preserveNamespaceOnError", + false); + + this.waitForNamespaceDeletion = Utils.getSystemPropertyOrEnvVar( + "josdk.it.waitForNamespaceDeletion", + true); + + this.oneNamespacePerClass = Utils.getSystemPropertyOrEnvVar( + "josdk.it.oneNamespacePerClass", + false); + } + } +} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java new file mode 100644 index 0000000000..6710b1fc92 --- /dev/null +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java @@ -0,0 +1,200 @@ +package io.javaoperatorsdk.operator.junit; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; + +public class E2EOperatorExtension extends AbstractOperatorExtension { + + private static final Logger LOGGER = LoggerFactory.getLogger(E2EOperatorExtension.class); + + private final List operatorDeployment; + private final Duration operatorDeploymentTimeout; + + private E2EOperatorExtension( + ConfigurationService configurationService, + List operatorDeployment, + Duration operatorDeploymentTimeout, + List infrastructure, + Duration infrastructureTimeout, + boolean preserveNamespaceOnError, + boolean waitForNamespaceDeletion, + boolean oneNamespacePerClass) { + super(configurationService, infrastructure, infrastructureTimeout, oneNamespacePerClass, + preserveNamespaceOnError, + waitForNamespaceDeletion); + this.operatorDeployment = operatorDeployment; + this.operatorDeploymentTimeout = operatorDeploymentTimeout; + } + + /** + * Creates a {@link Builder} to set up an {@link E2EOperatorExtension} instance. + * + * @return the builder. + */ + public static Builder builder() { + return new Builder(); + } + + @SuppressWarnings("unchecked") + protected void before(ExtensionContext context) { + LOGGER.info("Initializing integration test in namespace {}", namespace); + + kubernetesClient + .namespaces() + .create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); + + kubernetesClient + .resourceList(infrastructure) + .createOrReplace(); + kubernetesClient + .resourceList(infrastructure) + .waitUntilReady(infrastructureTimeout.toMillis(), TimeUnit.MILLISECONDS); + + final var crdPath = "./target/classes/META-INF/fabric8/"; + final var crdSuffix = "-v1.yml"; + + for (var crdFile : new File(crdPath).listFiles((ignored, name) -> name.endsWith(crdSuffix))) { + try (InputStream is = new FileInputStream(crdFile)) { + final var crd = kubernetesClient.load(is); + crd.createOrReplace(); + crd.waitUntilReady(2, TimeUnit.SECONDS); + LOGGER.debug("Applied CRD with name: {}", crd.get().get(0).getMetadata().getName()); + } catch (Exception ex) { + throw new IllegalStateException("Cannot apply CRD yaml: " + crdFile.getAbsolutePath(), ex); + } + } + + LOGGER.debug("Deploying the operator into Kubernetes"); + operatorDeployment.stream().forEach(hm -> { + hm.getMetadata().setNamespace(namespace); + if (hm.getKind().toLowerCase(Locale.ROOT).equals("clusterrolebinding")) { + var crb = (ClusterRoleBinding) hm; + for (var subject : crb.getSubjects()) { + subject.setNamespace(namespace); + } + } + }); + + kubernetesClient + .resourceList(operatorDeployment) + .inNamespace(namespace) + .createOrReplace(); + kubernetesClient + .resourceList(operatorDeployment) + .waitUntilReady(operatorDeploymentTimeout.toMillis(), TimeUnit.MILLISECONDS); + } + + protected void after(ExtensionContext context) { + if (namespace != null) { + if (preserveNamespaceOnError && context.getExecutionException().isPresent()) { + LOGGER.info("Preserving namespace {}", namespace); + } else { + kubernetesClient.resourceList(infrastructure).delete(); + kubernetesClient.resourceList(operatorDeployment).inNamespace(namespace).delete(); + LOGGER.info("Deleting namespace {} and stopping operator", namespace); + kubernetesClient.namespaces().withName(namespace).delete(); + if (waitForNamespaceDeletion) { + LOGGER.info("Waiting for namespace {} to be deleted", namespace); + Awaitility.await("namespace deleted") + .pollInterval(50, TimeUnit.MILLISECONDS) + .atMost(90, TimeUnit.SECONDS) + .until(() -> kubernetesClient.namespaces().withName(namespace).get() == null); + } + } + } + } + + @SuppressWarnings("rawtypes") + public static class Builder extends AbstractBuilder { + private final List operatorDeployment; + private Duration deploymentTimeout; + + protected Builder() { + super();; + this.operatorDeployment = new ArrayList<>(); + this.deploymentTimeout = Duration.ofMinutes(1); + } + + public Builder preserveNamespaceOnError(boolean value) { + this.preserveNamespaceOnError = value; + return this; + } + + public Builder waitForNamespaceDeletion(boolean value) { + this.waitForNamespaceDeletion = value; + return this; + } + + public Builder oneNamespacePerClass(boolean value) { + this.oneNamespacePerClass = value; + return this; + } + + public Builder withConfigurationService(ConfigurationService value) { + configurationService = value; + return this; + } + + public Builder withDeploymentTimeout(Duration value) { + deploymentTimeout = value; + return this; + } + + public Builder withInfrastructureTimeout(Duration value) { + infrastructureTimeout = value; + return this; + } + + public Builder withInfrastructure(List hm) { + infrastructure.addAll(hm); + return this; + } + + public Builder withInfrastructure(HasMetadata... hms) { + for (HasMetadata hm : hms) { + infrastructure.add(hm); + } + return this; + } + + public Builder withOperatorDeployment(List hm) { + operatorDeployment.addAll(hm); + return this; + } + + public Builder withOperatorDeployment(HasMetadata... hms) { + for (HasMetadata hm : hms) { + operatorDeployment.add(hm); + } + return this; + } + + public E2EOperatorExtension build() { + return new E2EOperatorExtension( + configurationService, + operatorDeployment, + deploymentTimeout, + infrastructure, + infrastructureTimeout, + preserveNamespaceOnError, + waitForNamespaceDeletion, + oneNamespacePerClass); + } + } +} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 3393de7a55..247ec9043b 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -1,30 +1,19 @@ package io.javaoperatorsdk.operator.junit; import java.io.InputStream; +import java.time.Duration; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.awaitility.Awaitility; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.NamespaceBuilder; -import io.fabric8.kubernetes.client.DefaultKubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; -import io.fabric8.kubernetes.client.dsl.Resource; -import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; -import io.fabric8.kubernetes.client.utils.Utils; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; @@ -35,36 +24,26 @@ import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override; -public class OperatorExtension - implements HasKubernetesClient, - BeforeAllCallback, - BeforeEachCallback, - AfterAllCallback, - AfterEachCallback { +public class OperatorExtension extends AbstractOperatorExtension { private static final Logger LOGGER = LoggerFactory.getLogger(OperatorExtension.class); - private final KubernetesClient kubernetesClient; - private final ConfigurationService configurationService; private final Operator operator; private final List reconcilers; - private final boolean preserveNamespaceOnError; - private final boolean waitForNamespaceDeletion; - - private String namespace; private OperatorExtension( ConfigurationService configurationService, List reconcilers, + List infrastructure, + Duration infrastructureTimeout, boolean preserveNamespaceOnError, - boolean waitForNamespaceDeletion) { - - this.kubernetesClient = new DefaultKubernetesClient(); - this.configurationService = configurationService; + boolean waitForNamespaceDeletion, + boolean oneNamespacePerClass) { + super(configurationService, infrastructure, infrastructureTimeout, oneNamespacePerClass, + preserveNamespaceOnError, + waitForNamespaceDeletion); this.reconcilers = reconcilers; this.operator = new Operator(this.kubernetesClient, this.configurationService); - this.preserveNamespaceOnError = preserveNamespaceOnError; - this.waitForNamespaceDeletion = waitForNamespaceDeletion; } /** @@ -76,35 +55,6 @@ public static Builder builder() { return new Builder(); } - @Override - public void beforeAll(ExtensionContext context) throws Exception { - before(context); - } - - @Override - public void beforeEach(ExtensionContext context) throws Exception { - before(context); - } - - @Override - public void afterAll(ExtensionContext context) throws Exception { - after(context); - } - - @Override - public void afterEach(ExtensionContext context) throws Exception { - after(context); - } - - @Override - public KubernetesClient getKubernetesClient() { - return kubernetesClient; - } - - public String getNamespace() { - return namespace; - } - @SuppressWarnings({"rawtypes"}) public List getReconcilers() { return operator.getControllers().stream() @@ -129,41 +79,20 @@ public T getControllerOfType(Class type) { () -> new IllegalArgumentException("Unable to find a reconciler of type: " + type)); } - public NonNamespaceOperation, Resource> resources( - Class type) { - return kubernetesClient.resources(type).inNamespace(namespace); - } - - public T get(Class type, String name) { - return kubernetesClient.resources(type).inNamespace(namespace).withName(name).get(); - } - - public T create(Class type, T resource) { - return kubernetesClient.resources(type).inNamespace(namespace).create(resource); - } - - public T replace(Class type, T resource) { - return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); - } - - public boolean delete(Class type, T resource) { - return kubernetesClient.resources(type).inNamespace(namespace).delete(resource); - } - @SuppressWarnings("unchecked") protected void before(ExtensionContext context) { - namespace = context.getRequiredTestClass().getSimpleName(); - namespace += "-"; - namespace += context.getRequiredTestMethod().getName(); - namespace = KubernetesResourceUtil.sanitizeName(namespace).toLowerCase(Locale.US); - namespace = namespace.substring(0, Math.min(namespace.length(), 63)); - LOGGER.info("Initializing integration test in namespace {}", namespace); kubernetesClient .namespaces() .create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); + kubernetesClient + .resourceList(infrastructure) + .createOrReplace(); + kubernetesClient + .resourceList(infrastructure) + .waitUntilReady(infrastructureTimeout.toMillis(), TimeUnit.MILLISECONDS); for (var ref : reconcilers) { final var config = configurationService.getConfigurationFor(ref.reconciler); @@ -191,6 +120,7 @@ protected void before(ExtensionContext context) { this.operator.register(ref.reconciler, oconfig.build()); } + LOGGER.debug("Starting the operator locally"); this.operator.start(); } @@ -199,6 +129,7 @@ protected void after(ExtensionContext context) { if (preserveNamespaceOnError && context.getExecutionException().isPresent()) { LOGGER.info("Preserving namespace {}", namespace); } else { + kubernetesClient.resourceList(infrastructure).delete(); LOGGER.info("Deleting namespace {} and stopping operator", namespace); kubernetesClient.namespaces().withName(namespace).delete(); if (waitForNamespaceDeletion) { @@ -219,23 +150,14 @@ protected void after(ExtensionContext context) { } @SuppressWarnings("rawtypes") - public static class Builder { + public static class Builder extends AbstractBuilder { private final List reconcilers; private ConfigurationService configurationService; - private boolean preserveNamespaceOnError; - private boolean waitForNamespaceDeletion; protected Builder() { + super(); this.configurationService = new BaseConfigurationService(Version.UNKNOWN); this.reconcilers = new ArrayList<>(); - - this.preserveNamespaceOnError = Utils.getSystemPropertyOrEnvVar( - "josdk.it.preserveNamespaceOnError", - false); - - this.waitForNamespaceDeletion = Utils.getSystemPropertyOrEnvVar( - "josdk.it.waitForNamespaceDeletion", - true); } public Builder preserveNamespaceOnError(boolean value) { @@ -248,11 +170,33 @@ public Builder waitForNamespaceDeletion(boolean value) { return this; } + public Builder oneNamespacePerClass(boolean value) { + this.oneNamespacePerClass = value; + return this; + } + public Builder withConfigurationService(ConfigurationService value) { configurationService = value; return this; } + public Builder withInfrastructureTimeout(Duration value) { + infrastructureTimeout = value; + return this; + } + + public Builder withInfrastructure(List hm) { + infrastructure.addAll(hm); + return this; + } + + public Builder withInfrastructure(HasMetadata... hms) { + for (HasMetadata hm : hms) { + infrastructure.add(hm); + } + return this; + } + @SuppressWarnings("rawtypes") public Builder withReconciler(Reconciler value) { reconcilers.add(new ReconcilerSpec(value, null)); @@ -279,8 +223,11 @@ public OperatorExtension build() { return new OperatorExtension( configurationService, reconcilers, + infrastructure, + infrastructureTimeout, preserveNamespaceOnError, - waitForNamespaceDeletion); + waitForNamespaceDeletion, + oneNamespacePerClass); } } diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index f38aa9bc17..bda0efa060 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -72,6 +72,12 @@ jackson-dataformat-yaml 2.13.1 + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index af77b6ce97..18a2c7e518 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -23,7 +23,7 @@ import static java.lang.String.format; -@ControllerConfiguration +@ControllerConfiguration(finalizerName = Constants.NO_FINALIZER) public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler, EventSourceInitializer { 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 f4c529426b..cf8284f179 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 @@ -1,18 +1,26 @@ package io.javaoperatorsdk.operator.sample; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.*; -import io.fabric8.kubernetes.api.model.apps.*; -import io.fabric8.kubernetes.client.*; -import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.ConfigBuilder; -import io.javaoperatorsdk.operator.Operator; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.LocalPortForward; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; +import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; +import io.javaoperatorsdk.operator.junit.OperatorExtension; import static java.util.concurrent.TimeUnit.MINUTES; import static org.awaitility.Awaitility.await; @@ -23,61 +31,102 @@ public class MySQLSchemaOperatorE2E { - final static String TEST_NS = "mysql-schema-test"; + final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); + + final static KubernetesClient client = new DefaultKubernetesClient(); + final static String MY_SQL_NS = "mysql"; - final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); + private static List infrastructure = new ArrayList<>(); + static { + infrastructure + .add(new NamespaceBuilder() + .withNewMetadata() + .withName(MY_SQL_NS) + .endMetadata() + .build()); + try { + infrastructure.addAll( + client.load(new FileInputStream("k8s/mysql-deployment.yaml")).get()); + infrastructure.addAll( + client.load(new FileInputStream("k8s/mysql-service.yaml")).get()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + boolean isLocal() { + String deployment = System.getProperty("test.deployment"); + boolean remote = (deployment != null && deployment.equals("remote")); + log.info("Running the operator " + (remote ? "remote" : "locally")); + return !remote; + } + + @RegisterExtension + AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new MySQLSchemaReconciler(client, + new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))) + .withInfrastructure(infrastructure) + .build() + : E2EOperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withOperatorDeployment( + client.load(new FileInputStream("k8s/operator.yaml")).get()) + .withInfrastructure(infrastructure) + .build(); + + + + public MySQLSchemaOperatorE2E() throws FileNotFoundException {} @Test public void test() throws IOException { - Config config = new ConfigBuilder().withNamespace(null).build(); - KubernetesClient client = new DefaultKubernetesClient(config); - - // Use this if you want to run the test without deploying the Operator to Kubernetes - if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) { - Operator operator = new Operator(client, DefaultConfigurationService.instance()); - MySQLDbConfig dbConfig = new MySQLDbConfig("mysql", null, "root", "password"); - operator.register(new MySQLSchemaReconciler(client, dbConfig)); - operator.start(); + // Opening a port-forward if running locally + LocalPortForward portForward = null; + if (isLocal()) { + String podName = client + .pods() + .inNamespace(MY_SQL_NS) + .withLabel("app", "mysql") + .list() + .getItems() + .get(0) + .getMetadata() + .getName(); + + portForward = client + .pods() + .inNamespace(MY_SQL_NS) + .withName(podName) + .portForward(3306, 3306); } MySQLSchema testSchema = new MySQLSchema(); testSchema.setMetadata(new ObjectMetaBuilder() .withName("mydb1") - .withNamespace(TEST_NS) + .withNamespace(operator.getNamespace()) .build()); testSchema.setSpec(new SchemaSpec()); testSchema.getSpec().setEncoding("utf8"); - Namespace testNs = new NamespaceBuilder().withMetadata( - new ObjectMetaBuilder().withName(TEST_NS).build()).build(); - - if (testNs != null) { - // We perform a pre-run cleanup instead of a post-run cleanup. This is to help with debugging - // test results when running against a persistent cluster. The test namespace would stay - // after the test run so we can check what's there, but it would be cleaned up during the next - // test run. - log.info("Cleanup: deleting test namespace {}", TEST_NS); - client.namespaces().delete(testNs); - await().atMost(5, MINUTES) - .until(() -> client.namespaces().withName(TEST_NS).get() == null); - } - - log.info("Creating test namespace {}", TEST_NS); - client.namespaces().create(testNs); - log.info("Creating test MySQLSchema object: {}", testSchema); client.resource(testSchema).createOrReplace(); log.info("Waiting 5 minutes for expected resources to be created and updated"); - await().atMost(5, MINUTES).untilAsserted(() -> { - MySQLSchema updatedSchema = client.resources(MySQLSchema.class).inNamespace(TEST_NS) - .withName(testSchema.getMetadata().getName()).get(); + await().atMost(1, MINUTES).ignoreExceptions().untilAsserted(() -> { + MySQLSchema updatedSchema = + client.resources(MySQLSchema.class).inNamespace(operator.getNamespace()) + .withName(testSchema.getMetadata().getName()).get(); assertThat(updatedSchema.getStatus(), is(notNullValue())); assertThat(updatedSchema.getStatus().getStatus(), equalTo("CREATED")); assertThat(updatedSchema.getStatus().getSecretName(), is(notNullValue())); assertThat(updatedSchema.getStatus().getUserName(), is(notNullValue())); }); + + if (portForward != null) { + portForward.close(); + } } } diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 9ab41d0e57..4d95bcee4e 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -57,6 +57,12 @@ 4.1.1 test + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 0f785feb02..719c435b08 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -28,7 +28,9 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -@ControllerConfiguration +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) public class WebappReconciler implements Reconciler, EventSourceInitializer { private KubernetesClient kubernetesClient; diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index e803b70aba..042f4973cf 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -1,18 +1,22 @@ package io.javaoperatorsdk.operator.sample; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.*; -import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; -import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; +import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; +import io.javaoperatorsdk.operator.junit.OperatorExtension; -import static java.util.concurrent.TimeUnit.*; +import static java.util.concurrent.TimeUnit.MINUTES; import static org.awaitility.Awaitility.await; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -21,75 +25,82 @@ public class TomcatOperatorE2E { - final static String TEST_NS = "tomcat-test"; - final static Logger log = LoggerFactory.getLogger(TomcatOperatorE2E.class); - @Test - public void test() { - Config config = new ConfigBuilder().withNamespace(null).build(); - KubernetesClient client = new DefaultKubernetesClient(config); + final static KubernetesClient client = new DefaultKubernetesClient(); - // Use this if you want to run the test without deploying the Operator to Kubernetes - if ("true".equals(System.getenv("RUN_OPERATOR_IN_TEST"))) { - Operator operator = new Operator(client, DefaultConfigurationService.instance()); - operator.register(new TomcatReconciler(client)); - operator.register(new WebappReconciler(client)); - operator.start(); - } + public TomcatOperatorE2E() throws FileNotFoundException {} + + final static int tomcatReplicas = 2; + + boolean isLocal() { + String deployment = System.getProperty("test.deployment"); + boolean remote = (deployment != null && deployment.equals("remote")); + log.info("Running the operator " + (remote ? "remote" : "locally")); + return !remote; + } + @RegisterExtension + AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new TomcatReconciler(client)) + .withReconciler(new WebappReconciler(client)) + .build() + : E2EOperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withOperatorDeployment( + client.load(new FileInputStream("k8s/operator.yaml")).get()) + .build(); + + Tomcat getTomcat() { Tomcat tomcat = new Tomcat(); tomcat.setMetadata(new ObjectMetaBuilder() .withName("test-tomcat1") - .withNamespace(TEST_NS) + .withNamespace(operator.getNamespace()) .build()); tomcat.setSpec(new TomcatSpec()); - tomcat.getSpec().setReplicas(3); + tomcat.getSpec().setReplicas(tomcatReplicas); tomcat.getSpec().setVersion(9); + return tomcat; + } + Webapp getWebapp() { Webapp webapp1 = new Webapp(); webapp1.setMetadata(new ObjectMetaBuilder() .withName("test-webapp1") - .withNamespace(TEST_NS) + .withNamespace(operator.getNamespace()) .build()); webapp1.setSpec(new WebappSpec()); webapp1.getSpec().setContextPath("webapp1"); - webapp1.getSpec().setTomcat(tomcat.getMetadata().getName()); + webapp1.getSpec().setTomcat(getTomcat().getMetadata().getName()); webapp1.getSpec().setUrl("http://tomcat.apache.org/tomcat-7.0-doc/appdev/sample/sample.war"); + return webapp1; + } - var tomcatClient = client.customResources(Tomcat.class); - var webappClient = client.customResources(Webapp.class); - - Namespace testNs = new NamespaceBuilder().withMetadata( - new ObjectMetaBuilder().withName(TEST_NS).build()).build(); - - if (testNs != null) { - // We perform a pre-run cleanup instead of a post-run cleanup. This is to help with debugging - // test results when running against a persistent cluster. The test namespace would stay - // after the test run so we can check what's there, but it would be cleaned up during the next - // test run. - log.info("Cleanup: deleting test namespace {}", TEST_NS); - client.namespaces().delete(testNs); - await().atMost(5, MINUTES) - .until(() -> client.namespaces().withName("tomcat-test").get() == null); - } - - log.info("Creating test namespace {}", TEST_NS); - client.namespaces().create(testNs); + @Test + public void test() { + var tomcat = getTomcat(); + var webapp1 = getWebapp(); + var tomcatClient = client.resources(Tomcat.class); + var webappClient = client.resources(Webapp.class); log.info("Creating test Tomcat object: {}", tomcat); - tomcatClient.inNamespace(TEST_NS).create(tomcat); + tomcatClient.inNamespace(operator.getNamespace()).create(tomcat); log.info("Creating test Webapp object: {}", webapp1); - webappClient.inNamespace(TEST_NS).create(webapp1); + webappClient.inNamespace(operator.getNamespace()).create(webapp1); log.info("Waiting 5 minutes for Tomcat and Webapp CR statuses to be updated"); await().atMost(5, MINUTES).untilAsserted(() -> { Tomcat updatedTomcat = - tomcatClient.inNamespace(TEST_NS).withName(tomcat.getMetadata().getName()).get(); + tomcatClient.inNamespace(operator.getNamespace()).withName(tomcat.getMetadata().getName()) + .get(); Webapp updatedWebapp = - webappClient.inNamespace(TEST_NS).withName(webapp1.getMetadata().getName()).get(); + webappClient.inNamespace(operator.getNamespace()) + .withName(webapp1.getMetadata().getName()).get(); assertThat(updatedTomcat.getStatus(), is(notNullValue())); - assertThat(updatedTomcat.getStatus().getReadyReplicas(), equalTo(3)); + assertThat(updatedTomcat.getStatus().getReadyReplicas(), equalTo(tomcatReplicas)); assertThat(updatedWebapp.getStatus(), is(notNullValue())); assertThat(updatedWebapp.getStatus().getDeployedArtifact(), is(notNullValue())); }); @@ -102,7 +113,7 @@ public void test() { try { log.info("Starting curl Pod to test if webapp was deployed correctly"); - Pod curlPod = client.run().inNamespace(TEST_NS) + Pod curlPod = client.run().inNamespace(operator.getNamespace()) .withRunConfig(new RunConfigBuilder() .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) .withName("curl") @@ -114,21 +125,24 @@ public void test() { await("wait-for-curl-pod-run").atMost(2, MINUTES) .until(() -> { String phase = - client.pods().inNamespace(TEST_NS).withName("curl").get().getStatus().getPhase(); + client.pods().inNamespace(operator.getNamespace()).withName("curl").get() + .getStatus().getPhase(); return phase.equals("Succeeded") || phase.equals("Failed"); }); String curlOutput = - client.pods().inNamespace(TEST_NS).withName(curlPod.getMetadata().getName()).getLog(); + client.pods().inNamespace(operator.getNamespace()) + .withName(curlPod.getMetadata().getName()).getLog(); log.info("Output from curl: '{}'", curlOutput); assertThat(curlOutput, equalTo("200")); } catch (KubernetesClientException ex) { throw new AssertionError(ex); } finally { log.info("Deleting curl Pod"); - client.pods().inNamespace(TEST_NS).withName("curl").delete(); + client.pods().inNamespace(operator.getNamespace()).withName("curl").delete(); await("wait-for-curl-pod-stop").atMost(1, MINUTES) - .until(() -> client.pods().inNamespace(TEST_NS).withName("curl").get() == null); + .until(() -> client.pods().inNamespace(operator.getNamespace()).withName("curl") + .get() == null); } }); } From 6ee3173854381d9aeb51fa111c6abbdcd3478dc1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 4 Feb 2022 15:18:33 +0100 Subject: [PATCH 0288/1608] refactor: clean-up dependent implementation (#906) * feat: add desired and matches methods on DependentResource This removes the need for Builder and Updater interfaces. * refactor: move classes to more coherent locations * refactor: remove DependentResourceControllerFactory * refactor: remove Cleaner interface * refactor: put the reconciling logic in DependentResourceController * refactor: return DependentResource.desired now returns Optional * chore: add @Ignore to mark internal Reconciler implementations --- .../api/config/ControllerConfiguration.java | 6 +- .../DefaultControllerConfiguration.java | 1 + .../operator/api/config/Dependent.java | 8 -- .../api/config/DependentResource.java | 15 -- .../api/config/dependent/Dependent.java | 10 ++ .../DependentResourceConfiguration.java | 3 +- .../{ => dependent}/KubernetesDependent.java | 2 +- ...ernetesDependentResourceConfiguration.java | 7 +- .../informer/InformerConfiguration.java | 3 +- .../reconciler/ControllerConfiguration.java | 4 +- .../api/reconciler/dependent/Builder.java | 9 -- .../api/reconciler/dependent/Cleaner.java | 9 -- .../dependent/DependentResource.java | 62 ++++++++ .../DependentResourceController.java | 94 ------------ .../DependentResourceControllerFactory.java | 26 ---- .../api/reconciler/dependent/Updater.java | 10 -- .../DependentResourceController.java | 134 ++++++++++++++++++ .../dependent/DependentResourceManager.java | 110 +++++--------- ...KubernetesDependentResourceController.java | 20 ++- .../source/informer/InformerEventSource.java | 1 + .../runtime/AnnotationConfiguration.java | 15 +- ...formerEventSourceTestCustomReconciler.java | 4 +- .../sample/MySQLSchemaReconciler.java | 23 ++- .../sample/SchemaDependentResource.java | 12 +- .../sample/DeploymentDependentResource.java | 28 ++-- .../sample/ServiceDependentResource.java | 30 ++-- .../sample/TomcatDependentResource.java | 2 +- .../operator/sample/TomcatReconciler.java | 2 +- .../operator/sample/WebappReconciler.java | 2 +- 29 files changed, 327 insertions(+), 325 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/{ => dependent}/DependentResourceConfiguration.java (63%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/{ => dependent}/KubernetesDependent.java (95%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/{reconciler => config}/dependent/KubernetesDependentResourceConfiguration.java (90%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{processing/event/source => api/config}/informer/InformerConfiguration.java (97%) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/KubernetesDependentResourceController.java (80%) 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 717e339514..c077a7876d 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 @@ -7,8 +7,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceControllerFactory; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -54,10 +54,6 @@ default List getDependentResources() { return Collections.emptyList(); } - default DependentResourceControllerFactory dependentFactory() { - return new DependentResourceControllerFactory<>() {}; - } - default Optional reconciliationMaxInterval() { return Optional.of(Duration.ofHours(10L)); } 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 index 41bacbbbce..188311cfdb 100644 --- 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 @@ -7,6 +7,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class DefaultControllerConfiguration diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java deleted file mode 100644 index ed013fafb0..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Dependent.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -public @interface Dependent { - - Class resourceType(); - - Class type(); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java deleted file mode 100644 index f4d5df89fb..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResource.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.javaoperatorsdk.operator.api.config; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -public interface DependentResource { - default EventSource initEventSource(EventSourceContext

context) { - throw new IllegalStateException("Must be implemented if not automatically provided by the SDK"); - }; - - default Class resourceType() { - return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java new file mode 100644 index 0000000000..017f4363a6 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; + +public @interface Dependent { + + Class resourceType(); + + Class type(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java similarity index 63% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java index 15911e0418..c0f428c12d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/DependentResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java @@ -1,6 +1,7 @@ -package io.javaoperatorsdk.operator.api.config; +package io.javaoperatorsdk.operator.api.config.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; public interface DependentResourceConfiguration { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java similarity index 95% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java index 5b9fbd38f3..e7d2ad3158 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.config; +package io.javaoperatorsdk.operator.api.config.dependent; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java similarity index 90% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java index 23d37881b4..6872322e4f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java @@ -1,14 +1,13 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.api.config.dependent; import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.DependentResource; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; public interface KubernetesDependentResourceConfiguration extends InformerConfiguration, DependentResourceConfiguration { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java similarity index 97% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index 087e206f8b..c4d747a990 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.processing.event.source.informer; +package io.javaoperatorsdk.operator.api.config.informer; import java.util.Collections; import java.util.Objects; @@ -12,6 +12,7 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; public interface InformerConfiguration extends ResourceConfiguration { 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 4db09c9cc8..85eed38025 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 @@ -6,8 +6,8 @@ import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; -import io.javaoperatorsdk.operator.api.config.Dependent; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.dependent.DependentResourceController; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java deleted file mode 100644 index 4dfaf91e64..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Builder.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -@FunctionalInterface -public interface Builder { - R buildFor(P primary, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java deleted file mode 100644 index a6a3d691be..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Cleaner.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -public interface Cleaner { - - void delete(R fetched, P primary, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java new file mode 100644 index 0000000000..df144ade1f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public interface DependentResource { + default EventSource initEventSource(EventSourceContext

context) { + throw new IllegalStateException("Must be implemented if not automatically provided by the SDK"); + } + + @SuppressWarnings("unchecked") + default Class resourceType() { + return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); + } + + default void delete(R fetched, P primary, Context context) {} + + /** + * Computes the desired state of the dependent based on the state provided by the specified + * primary resource. + * + * The default implementation returns {@code empty} which corresponds to the case where the + * associated dependent should never be created by the associated reconciler or that the global + * state of the cluster doesn't allow for the resource to be created at this point. + * + * @param primary the primary resource associated with the reconciliation process + * @param context the {@link Context} associated with the reconciliation process + * @return an instance of the dependent resource matching the desired state specified by the + * primary resource or {@code empty} if the dependent shouldn't be created at this point + * (or ever) + */ + default Optional desired(P primary, Context context) { + return Optional.empty(); + } + + /** + * Checks whether the actual resource as fetched from the cluster matches the desired state + * expressed by the specified primary resource. + * + * The default implementation always return {@code true}, which corresponds to the behavior where + * the dependent never needs to be updated after it's been created. + * + * Note that failure to properly implement this method will lead to infinite loops. In particular, + * for typical Kubernetes resource implementations, simply calling + * {@code desired(primary, context).equals(actual)} is not enough because metadata will usually be + * different. + * + * @param actual the current state of the resource as fetched from the cluster + * @param primary the primary resource associated with the reconciliation request + * @param context the {@link Context} associated with the reconciliation request + * @return {@code true} if the actual state of the resource matches the desired state expressed by + * the specified primary resource, {@code false} otherwise + */ + default boolean match(R actual, P primary, Context context) { + return true; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java deleted file mode 100644 index 21cd16a32f..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceController.java +++ /dev/null @@ -1,94 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.DependentResource; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -public class DependentResourceController> - implements DependentResource, Builder, Updater, Persister, - Cleaner { - - private final Builder builder; - private final Updater updater; - private final Cleaner cleaner; - private final Persister persister; - private final DependentResource delegate; - private final C configuration; - - @SuppressWarnings("unchecked") - public DependentResourceController(DependentResource delegate, C configuration) { - this.delegate = delegate; - builder = (delegate instanceof Builder) ? (Builder) delegate : null; - updater = (delegate instanceof Updater) ? (Updater) delegate : null; - cleaner = (delegate instanceof Cleaner) ? (Cleaner) delegate : null; - persister = initPersister(delegate); - this.configuration = configuration; - } - - @SuppressWarnings("unchecked") - protected Persister initPersister(DependentResource delegate) { - if (delegate instanceof Persister) { - return (Persister) delegate; - } else { - throw new IllegalArgumentException( - "DependentResource '" + delegate.getClass().getName() + "' must implement Persister"); - } - } - - public String descriptionFor(R resource) { - return resource.toString(); - } - - @Override - public R buildFor(P primary, Context context) { - return builder.buildFor(primary, context); - } - - @Override - public R update(R fetched, P primary, Context context) { - return updater.update(fetched, primary, context); - } - - @Override - public void delete(R fetched, P primary, Context context) { - cleaner.delete(fetched, primary, context); - } - - public Class getResourceType() { - return delegate.resourceType(); - } - - @Override - public EventSource initEventSource(EventSourceContext

context) { - return delegate.initEventSource(context); - } - - public boolean creatable() { - return builder != null; - } - - public boolean updatable() { - return updater != null; - } - - public boolean deletable() { - return cleaner != null; - } - - @Override - public void createOrReplace(R dependentResource, Context context) { - persister.createOrReplace(dependentResource, context); - } - - @Override - public R getFor(P primary, Context context) { - return persister.getFor(primary, context); - } - - public C getConfiguration() { - return configuration; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java deleted file mode 100644 index dd11a7a706..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceControllerFactory.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import java.lang.reflect.InvocationTargetException; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; - -public interface DependentResourceControllerFactory

{ - - default , R, C extends DependentResourceConfiguration> T from( - C config) { - try { - final var dependentResource = config.getDependentResourceClass().getConstructor() - .newInstance(); - if (config instanceof KubernetesDependentResourceConfiguration) { - return (T) new KubernetesDependentResourceController(dependentResource, - (KubernetesDependentResourceConfiguration) config); - } else { - return (T) new DependentResourceController(dependentResource, config); - } - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException - | IllegalAccessException e) { - throw new IllegalArgumentException(e); - } - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java deleted file mode 100644 index c92f3c5ced..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -@FunctionalInterface -public interface Updater { - - R update(R fetched, P primary, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java new file mode 100644 index 0000000000..d0004111af --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java @@ -0,0 +1,134 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.Ignore; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +@Ignore +public class DependentResourceController> + implements DependentResource, Persister, Reconciler

{ + + private static final Logger log = LoggerFactory.getLogger(DependentResourceController.class); + + private final Persister persister; + private final DependentResource delegate; + private final C configuration; + + public DependentResourceController(DependentResource delegate, C configuration) { + this.delegate = delegate; + persister = initPersister(delegate); + this.configuration = configuration; + } + + @Override + public Class resourceType() { + return delegate.resourceType(); + } + + @Override + public boolean match(R actual, P primary, Context context) { + return delegate.match(actual, primary, context); + } + + @Override + public Optional desired(P primary, Context context) { + return delegate.desired(primary, context); + } + + @Override + public void delete(R fetched, P primary, Context context) { + delegate.delete(fetched, primary, context); + } + + @SuppressWarnings("unchecked") + protected Persister initPersister(DependentResource delegate) { + if (delegate instanceof Persister) { + return (Persister) delegate; + } else { + throw new IllegalArgumentException( + "DependentResource '" + delegate.getClass().getName() + "' must implement Persister"); + } + } + + public String descriptionFor(R resource) { + return resource.toString(); + } + + public Class getResourceType() { + return delegate.resourceType(); + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + return delegate.initEventSource(context); + } + + @Override + public void createOrReplace(R dependentResource, Context context) { + persister.createOrReplace(dependentResource, context); + } + + @Override + public R getFor(P primary, Context context) { + return persister.getFor(primary, context); + } + + public C getConfiguration() { + return configuration; + } + + @Override + public UpdateControl

reconcile(P resource, Context context) { + var actual = getFor(resource, context); + if (actual == null || !match(actual, resource, context)) { + final var desired = desired(resource, context); + desired.ifPresent(d -> createOrReplaceDependent(resource, d, context)); + } + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(P primary, Context context) { + var dependent = getFor(primary, context); + if (dependent != null) { + delete(dependent, primary, context); + logOperationInfo(primary, dependent, "Deleting"); + } else { + log.info("Ignoring already deleted {} for '{}' {}", + getResourceType().getName(), + primary.getMetadata().getName(), + primary.getKind()); + } + return Reconciler.super.cleanup(primary, context); + } + + protected void createOrReplaceDependent(P primary, R dependent, Context context) { + logOperationInfo(primary, dependent, "Reconciling"); + + // commit the changes + // todo: add metrics timing for dependent resource + createOrReplace(dependent, context); + } + + private void logOperationInfo(P resource, R dependentResource, String operationDescription) { + if (log.isInfoEnabled()) { + log.info("{} {} for '{}' {}", operationDescription, + descriptionFor(dependentResource), + resource.getMetadata().getName(), + resource.getKind()); + } + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 911805b396..8ab04d7d09 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -1,14 +1,13 @@ package io.javaoperatorsdk.operator.processing.dependent; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -18,36 +17,32 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceController; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceController; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"rawtypes", "unchecked"}) @Ignore -public class DependentResourceManager implements EventSourceInitializer, - EventSourceContextInjector, Reconciler { - - private static final Logger log = LoggerFactory.getLogger(DependentResourceManager.class); - - private final Reconciler reconciler; - private final ControllerConfiguration configuration; +public class DependentResourceManager

implements EventSourceInitializer

, + EventSourceContextInjector, Reconciler

{ + private final Reconciler

reconciler; + private final ControllerConfiguration

configuration; private List dependents; - public DependentResourceManager(Controller controller) { + public DependentResourceManager(Controller

controller) { this.reconciler = controller.getReconciler(); this.configuration = controller.getConfiguration(); } @Override - public List prepareEventSources(EventSourceContext context) { + public List prepareEventSources(EventSourceContext

context) { final List configured = configuration.getDependentResources(); dependents = new ArrayList<>(configured.size()); List sources = new ArrayList<>(configured.size() + 5); configured.forEach(dependent -> { - final var dependentResourceController = configuration.dependentFactory().from(dependent); + final var dependentResourceController = from(dependent); dependents.add(dependentResourceController); sources.add(dependentResourceController.initEventSource(context)); }); @@ -64,80 +59,41 @@ public void injectInto(EventSourceContext context) { } @Override - public UpdateControl reconcile(R resource, Context context) { + public UpdateControl

reconcile(P resource, Context context) { initContextIfNeeded(resource, context); - - dependents.stream() - .filter(dependent -> dependent.creatable() || dependent.updatable()) - .forEach(dependent -> { - var dependentResource = dependent.getFor(resource, context); - if (dependent.creatable() && dependentResource == null) { - // we need to create the dependent - dependentResource = dependent.buildFor(resource, context); - createOrReplaceDependent(resource, context, dependent, dependentResource, "Creating"); - } else if (dependent.updatable()) { - dependentResource = dependent.update(dependentResource, resource, context); - createOrReplaceDependent(resource, context, dependent, dependentResource, "Updating"); - } else { - logOperationInfo(resource, dependent, dependentResource, "Ignoring"); - } - }); - + dependents.forEach(dependent -> dependent.reconcile(resource, context)); return UpdateControl.noUpdate(); } @Override - public DeleteControl cleanup(R resource, Context context) { + public DeleteControl cleanup(P resource, Context context) { initContextIfNeeded(resource, context); - - dependents.stream() - .filter(DependentResourceController::deletable) - .forEach(dependent -> { - var dependentResource = dependent.getFor(resource, context); - if (dependentResource != null) { - dependent.delete(dependentResource, resource, context); - logOperationInfo(resource, dependent, dependentResource, "Deleting"); - } else { - log.info("Ignoring already deleted {} for '{}' {}", - dependent.getResourceType().getName(), - resource.getMetadata().getName(), - configuration.getResourceTypeName()); - } - }); - + dependents.forEach(dependent -> dependent.cleanup(resource, context)); return Reconciler.super.cleanup(resource, context); } - private void createOrReplaceDependent(R primaryResource, - Context context, DependentResourceController dependentController, - Object dependentResource, String operationDescription) { - // add owner reference if needed - if (dependentResource instanceof HasMetadata - && ((KubernetesDependentResourceController) dependentController).owned()) { - ((HasMetadata) dependentResource).addOwnerReference(primaryResource); - } - logOperationInfo(primaryResource, dependentController, dependentResource, operationDescription); - - // commit the changes - // todo: add metrics timing for dependent resource - dependentController.createOrReplace(dependentResource, context); - } - - private void logOperationInfo(R resource, DependentResourceController dependent, - Object dependentResource, String operationDescription) { - if (log.isInfoEnabled()) { - log.info("{} {} for '{}' {}", operationDescription, - dependent.descriptionFor(dependentResource), - resource.getMetadata().getName(), - configuration.getResourceTypeName()); + private void initContextIfNeeded(P resource, Context context) { + if (reconciler instanceof ContextInitializer) { + final var initializer = (ContextInitializer

) reconciler; + initializer.initContext(resource, context); } } - private void initContextIfNeeded(R resource, Context context) { - if (reconciler instanceof ContextInitializer) { - final var initializer = (ContextInitializer) reconciler; - initializer.initContext(resource, context); + private DependentResourceController from(DependentResourceConfiguration config) { + try { + final var dependentResource = + (DependentResource) config.getDependentResourceClass().getConstructor() + .newInstance(); + if (config instanceof KubernetesDependentResourceConfiguration) { + return new KubernetesDependentResourceController(dependentResource, + (KubernetesDependentResourceConfiguration) config); + } else { + return new DependentResourceController(dependentResource, config); + } + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException + | IllegalAccessException e) { + throw new IllegalArgumentException(e); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java similarity index 80% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java index 172e9cb0d9..63d533193b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java @@ -1,16 +1,20 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; 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.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +@Ignore public class KubernetesDependentResourceController extends DependentResourceController> { @@ -19,6 +23,7 @@ public class KubernetesDependentResourceController informer; + @SuppressWarnings("unchecked") public KubernetesDependentResourceController(DependentResource delegate, KubernetesDependentResourceConfiguration configuration) { super(delegate, configuration); @@ -41,6 +46,7 @@ public KubernetesDependentResourceController(DependentResource delegate, configuration.getDependentResourceClass()); } + @SuppressWarnings("unchecked") @Override protected Persister initPersister(DependentResource delegate) { return (delegate instanceof Persister) ? (Persister) delegate : this; @@ -73,4 +79,12 @@ public R getFor(P primary, Context context) { public boolean owned() { return getConfiguration().isOwned(); } + + @Override + protected void createOrReplaceDependent(P primary, R dependent, Context context) { + if (owned()) { + dependent.addOwnerReference(primary); + } + super.createOrReplaceDependent(primary, dependent, context); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index fa9e516585..3def733bd8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java index 5d10762f35..19e31e932b 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java @@ -11,16 +11,16 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.Dependent; -import io.javaoperatorsdk.operator.api.config.DependentResource; -import io.javaoperatorsdk.operator.api.config.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.KubernetesDependent; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; 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.informer.InformerConfiguration; @SuppressWarnings("rawtypes") public class AnnotationConfiguration @@ -159,7 +159,8 @@ public List getDependentResources() { if (HasMetadata.class.isAssignableFrom(resourceType)) { final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); final var namespaces = - valueOrDefault(kubeDependent, KubernetesDependent::namespaces, new String[] {}); + valueOrDefault(kubeDependent, KubernetesDependent::namespaces, + this.getNamespaces().toArray(new String[0])); final var labelSelector = valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 2a04b85d19..33128a6b8c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -8,12 +8,12 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.api.config.Dependent; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; 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.DependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index ecc2aa9be7..ff4fcad131 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -9,8 +9,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.javaoperatorsdk.operator.api.config.Dependent; -import io.javaoperatorsdk.operator.api.config.DependentResource; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @@ -20,7 +19,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.SecretDependentResource; import io.javaoperatorsdk.operator.sample.schema.Schema; @@ -51,12 +50,15 @@ public MySQLSchemaReconciler(MySQLDbConfig mysqlDbConfig) { this.mysqlDbConfig = mysqlDbConfig; } - public static class SecretDependentResource - implements DependentResource, Builder { + public static class SecretDependentResource implements DependentResource { + + private static String encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } @Override - public Secret buildFor(MySQLSchema schema, Context context) { - return new SecretBuilder() + public Optional desired(MySQLSchema schema, Context context) { + return Optional.of(new SecretBuilder() .withNewMetadata() .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) .withNamespace(schema.getMetadata().getNamespace()) @@ -65,14 +67,11 @@ public Secret buildFor(MySQLSchema schema, Context context) { context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) .addToData("MYSQL_PASSWORD", encode( context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) - .build(); - } - - private static String encode(String value) { - return Base64.getEncoder().encodeToString(value.getBytes()); + .build()); } } + @SuppressWarnings("rawtypes") @Override public void injectInto(EventSourceContext context) { context.put(MYSQL_DB_CONFIG, mysqlDbConfig); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index de4b0ece58..8ba9612029 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -3,12 +3,11 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Optional; -import io.javaoperatorsdk.operator.api.config.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; @@ -18,8 +17,7 @@ import static java.lang.String.format; public class SchemaDependentResource - implements DependentResource, Builder, - Cleaner, Persister { + implements DependentResource, Persister { private static final int POLL_PERIOD = 500; private MySQLDbConfig dbConfig; @@ -33,7 +31,7 @@ public EventSource initEventSource(EventSourceContext context) { } @Override - public Schema buildFor(MySQLSchema primary, Context context) { + public Optional desired(MySQLSchema primary, Context context) { try (Connection connection = getConnection()) { final var schema = SchemaService.createSchemaAndRelatedUser( connection, @@ -44,7 +42,7 @@ public Schema buildFor(MySQLSchema primary, Context context) { // put the newly built schema in the context to let the reconciler know we just built it context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); - return schema; + return Optional.of(schema); } catch (SQLException e) { MySQLSchemaReconciler.log.error("Error while creating Schema", e); throw new IllegalStateException(e); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 245011a1cf..58200df988 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -1,21 +1,20 @@ package io.javaoperatorsdk.operator.sample; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; -import io.javaoperatorsdk.operator.api.config.DependentResource; -import io.javaoperatorsdk.operator.api.config.KubernetesDependent; +import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") public class DeploymentDependentResource - implements DependentResource, Builder, - Updater { + implements DependentResource { @Override - public Deployment buildFor(Tomcat tomcat, Context context) { + public Optional desired(Tomcat tomcat, Context context) { Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); @@ -35,18 +34,21 @@ public Deployment buildFor(Tomcat tomcat, Context context) { // make sure label selector matches label (which has to be matched by service selector too) .editMetadata().addToLabels("app", tomcatName).endMetadata() .editSpec() - .editFirstContainer().withImage("tomcat:" + tomcat.getSpec().getVersion()).endContainer() + .editFirstContainer().withImage(tomcatImage(tomcat)).endContainer() .endSpec() .endTemplate() .endSpec() .build(); - return deployment; + return Optional.of(deployment); + } + + private String tomcatImage(Tomcat tomcat) { + return "tomcat:" + tomcat.getSpec().getVersion(); } @Override - public Deployment update(Deployment fetched, Tomcat tomcat, Context context) { - return new DeploymentBuilder(fetched).editSpec().editTemplate().editSpec().editFirstContainer() - .withImage("tomcat:" + tomcat.getSpec().getVersion()) - .endContainer().endSpec().endTemplate().endSpec().build(); + public boolean match(Deployment fetched, Tomcat tomcat, Context context) { + return fetched.getSpec().getTemplate().getSpec().getContainers().stream() + .findFirst().map(c -> tomcatImage(tomcat).equals(c.getImage())).orElse(false); } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index b90bd0e6e2..52d043203b 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -1,28 +1,26 @@ package io.javaoperatorsdk.operator.sample; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.javaoperatorsdk.operator.api.config.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Builder; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -public class ServiceDependentResource - implements DependentResource, Builder { +public class ServiceDependentResource implements DependentResource { @Override - public Service buildFor(Tomcat tomcat, Context context) { + public Optional desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); - final Service service = - new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) - .editMetadata() - .withName(tomcatMetadata.getName()) - .withNamespace(tomcatMetadata.getNamespace()) - .endMetadata() - .editSpec() - .addToSelector("app", tomcatMetadata.getName()) - .endSpec() - .build(); - return service; + return Optional.of(new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) + .editMetadata() + .withName(tomcatMetadata.getName()) + .withNamespace(tomcatMetadata.getNamespace()) + .endMetadata() + .editSpec() + .addToSelector("app", tomcatMetadata.getName()) + .endSpec() + .build()); } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java index 9dd49466ab..abb4e62358 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java @@ -3,8 +3,8 @@ import java.util.Set; import java.util.stream.Collectors; -import io.javaoperatorsdk.operator.api.config.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 32f7927f44..d2d2a6c96c 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -11,7 +11,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; import io.fabric8.kubernetes.client.utils.Serialization; -import io.javaoperatorsdk.operator.api.config.Dependent; +import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 9e1f6f677f..8f7b682d62 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -17,6 +17,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -28,7 +29,6 @@ import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; @ControllerConfiguration From 817f8ca5f4565ddc78f6d813179e31e76658effb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 4 Feb 2022 15:55:32 +0100 Subject: [PATCH 0289/1608] refactor: share more code (#910) --- .../junit/AbstractOperatorExtension.java | 97 ++++++++++++++++-- .../operator/junit/E2EOperatorExtension.java | 91 +++-------------- .../operator/junit/OperatorExtension.java | 98 +++---------------- 3 files changed, 116 insertions(+), 170 deletions(-) diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java index fb204e8c57..839b550231 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -2,14 +2,20 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; import org.junit.jupiter.api.extension.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; @@ -26,7 +32,9 @@ public abstract class AbstractOperatorExtension implements HasKubernetesClient, AfterAllCallback, AfterEachCallback { - protected final KubernetesClient kubernetesClient; + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractOperatorExtension.class); + + private final KubernetesClient kubernetesClient; protected final ConfigurationService configurationService; protected final List infrastructure; protected Duration infrastructureTimeout; @@ -55,22 +63,22 @@ protected AbstractOperatorExtension( @Override - public void beforeAll(ExtensionContext context) throws Exception { + public void beforeAll(ExtensionContext context) { beforeAllImpl(context); } @Override - public void beforeEach(ExtensionContext context) throws Exception { + public void beforeEach(ExtensionContext context) { beforeEachImpl(context); } @Override - public void afterAll(ExtensionContext context) throws Exception { + public void afterAll(ExtensionContext context) { afterAllImpl(context); } @Override - public void afterEach(ExtensionContext context) throws Exception { + public void afterEach(ExtensionContext context) { afterEachImpl(context); } @@ -100,6 +108,7 @@ public T replace(Class type, T resource) { return kubernetesClient.resources(type).inNamespace(namespace).replace(resource); } + @SuppressWarnings("unchecked") public boolean delete(Class type, T resource) { return kubernetesClient.resources(type).inNamespace(namespace).delete(resource); } @@ -130,7 +139,20 @@ protected void beforeEachImpl(ExtensionContext context) { } } - protected abstract void before(ExtensionContext context); + protected void before(ExtensionContext context) { + LOGGER.info("Initializing integration test in namespace {}", namespace); + + kubernetesClient + .namespaces() + .create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); + + kubernetesClient + .resourceList(infrastructure) + .createOrReplace(); + kubernetesClient + .resourceList(infrastructure) + .waitUntilReady(infrastructureTimeout.toMillis(), TimeUnit.MILLISECONDS); + } protected void afterAllImpl(ExtensionContext context) { if (oneNamespacePerClass) { @@ -144,9 +166,32 @@ protected void afterEachImpl(ExtensionContext context) { } } - protected abstract void after(ExtensionContext context); + protected void after(ExtensionContext context) { + if (namespace != null) { + if (preserveNamespaceOnError && context.getExecutionException().isPresent()) { + LOGGER.info("Preserving namespace {}", namespace); + } else { + kubernetesClient.resourceList(infrastructure).delete(); + deleteOperator(); + LOGGER.info("Deleting namespace {} and stopping operator", namespace); + kubernetesClient.namespaces().withName(namespace).delete(); + if (waitForNamespaceDeletion) { + LOGGER.info("Waiting for namespace {} to be deleted", namespace); + Awaitility.await("namespace deleted") + .pollInterval(50, TimeUnit.MILLISECONDS) + .atMost(90, TimeUnit.SECONDS) + .until(() -> kubernetesClient.namespaces().withName(namespace).get() == null); + } + } + } + } + + protected void deleteOperator() { + // nothing to do by default: only needed if the operator is deployed to the cluster + } - public static abstract class AbstractBuilder { + @SuppressWarnings("unchecked") + public static abstract class AbstractBuilder> { protected ConfigurationService configurationService; protected final List infrastructure; protected Duration infrastructureTimeout; @@ -172,5 +217,41 @@ protected AbstractBuilder() { "josdk.it.oneNamespacePerClass", false); } + + public T preserveNamespaceOnError(boolean value) { + this.preserveNamespaceOnError = value; + return (T) this; + } + + public T waitForNamespaceDeletion(boolean value) { + this.waitForNamespaceDeletion = value; + return (T) this; + } + + public T oneNamespacePerClass(boolean value) { + this.oneNamespacePerClass = value; + return (T) this; + } + + public T withConfigurationService(ConfigurationService value) { + configurationService = value; + return (T) this; + } + + public T withInfrastructureTimeout(Duration value) { + infrastructureTimeout = value; + return (T) this; + } + + public T withInfrastructure(List hm) { + infrastructure.addAll(hm); + return (T) this; + } + + public T withInfrastructure(HasMetadata... hms) { + infrastructure.addAll(Arrays.asList(hms)); + return (T) this; + } + } } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java index 6710b1fc92..91fecafc2e 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java @@ -5,17 +5,17 @@ import java.io.InputStream; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding; import io.javaoperatorsdk.operator.api.config.ConfigurationService; @@ -51,25 +51,15 @@ public static Builder builder() { return new Builder(); } - @SuppressWarnings("unchecked") protected void before(ExtensionContext context) { - LOGGER.info("Initializing integration test in namespace {}", namespace); - - kubernetesClient - .namespaces() - .create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); - - kubernetesClient - .resourceList(infrastructure) - .createOrReplace(); - kubernetesClient - .resourceList(infrastructure) - .waitUntilReady(infrastructureTimeout.toMillis(), TimeUnit.MILLISECONDS); + super.before(context); final var crdPath = "./target/classes/META-INF/fabric8/"; final var crdSuffix = "-v1.yml"; - for (var crdFile : new File(crdPath).listFiles((ignored, name) -> name.endsWith(crdSuffix))) { + final var kubernetesClient = getKubernetesClient(); + for (var crdFile : Objects + .requireNonNull(new File(crdPath).listFiles((ignored, name) -> name.endsWith(crdSuffix)))) { try (InputStream is = new FileInputStream(crdFile)) { final var crd = kubernetesClient.load(is); crd.createOrReplace(); @@ -81,7 +71,7 @@ protected void before(ExtensionContext context) { } LOGGER.debug("Deploying the operator into Kubernetes"); - operatorDeployment.stream().forEach(hm -> { + operatorDeployment.forEach(hm -> { hm.getMetadata().setNamespace(namespace); if (hm.getKind().toLowerCase(Locale.ROOT).equals("clusterrolebinding")) { var crb = (ClusterRoleBinding) hm; @@ -100,88 +90,33 @@ protected void before(ExtensionContext context) { .waitUntilReady(operatorDeploymentTimeout.toMillis(), TimeUnit.MILLISECONDS); } - protected void after(ExtensionContext context) { - if (namespace != null) { - if (preserveNamespaceOnError && context.getExecutionException().isPresent()) { - LOGGER.info("Preserving namespace {}", namespace); - } else { - kubernetesClient.resourceList(infrastructure).delete(); - kubernetesClient.resourceList(operatorDeployment).inNamespace(namespace).delete(); - LOGGER.info("Deleting namespace {} and stopping operator", namespace); - kubernetesClient.namespaces().withName(namespace).delete(); - if (waitForNamespaceDeletion) { - LOGGER.info("Waiting for namespace {} to be deleted", namespace); - Awaitility.await("namespace deleted") - .pollInterval(50, TimeUnit.MILLISECONDS) - .atMost(90, TimeUnit.SECONDS) - .until(() -> kubernetesClient.namespaces().withName(namespace).get() == null); - } - } - } + @Override + protected void deleteOperator() { + getKubernetesClient().resourceList(operatorDeployment).inNamespace(namespace).delete(); } - @SuppressWarnings("rawtypes") - public static class Builder extends AbstractBuilder { + public static class Builder extends AbstractBuilder { private final List operatorDeployment; private Duration deploymentTimeout; protected Builder() { - super();; + super(); this.operatorDeployment = new ArrayList<>(); this.deploymentTimeout = Duration.ofMinutes(1); } - public Builder preserveNamespaceOnError(boolean value) { - this.preserveNamespaceOnError = value; - return this; - } - - public Builder waitForNamespaceDeletion(boolean value) { - this.waitForNamespaceDeletion = value; - return this; - } - - public Builder oneNamespacePerClass(boolean value) { - this.oneNamespacePerClass = value; - return this; - } - - public Builder withConfigurationService(ConfigurationService value) { - configurationService = value; - return this; - } - public Builder withDeploymentTimeout(Duration value) { deploymentTimeout = value; return this; } - public Builder withInfrastructureTimeout(Duration value) { - infrastructureTimeout = value; - return this; - } - - public Builder withInfrastructure(List hm) { - infrastructure.addAll(hm); - return this; - } - - public Builder withInfrastructure(HasMetadata... hms) { - for (HasMetadata hm : hms) { - infrastructure.add(hm); - } - return this; - } - public Builder withOperatorDeployment(List hm) { operatorDeployment.addAll(hm); return this; } public Builder withOperatorDeployment(HasMetadata... hms) { - for (HasMetadata hm : hms) { - operatorDeployment.add(hm); - } + operatorDeployment.addAll(Arrays.asList(hms)); return this; } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 247ec9043b..76d848c896 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -6,24 +6,22 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.awaitility.Awaitility; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.Version; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.retry.Retry; import static io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override; +@SuppressWarnings("rawtypes") public class OperatorExtension extends AbstractOperatorExtension { private static final Logger LOGGER = LoggerFactory.getLogger(OperatorExtension.class); @@ -43,7 +41,7 @@ private OperatorExtension( preserveNamespaceOnError, waitForNamespaceDeletion); this.reconcilers = reconcilers; - this.operator = new Operator(this.kubernetesClient, this.configurationService); + this.operator = new Operator(getKubernetesClient(), this.configurationService); } /** @@ -55,23 +53,20 @@ public static Builder builder() { return new Builder(); } - @SuppressWarnings({"rawtypes"}) + private Stream reconcilers() { + return operator.getControllers().stream().map(Controller::getReconciler); + } + public List getReconcilers() { - return operator.getControllers().stream() - .map(Controller::getReconciler) - .collect(Collectors.toUnmodifiableList()); + return reconcilers().collect(Collectors.toUnmodifiableList()); } public Reconciler getFirstReconciler() { - return operator.getControllers().stream() - .map(Controller::getReconciler) - .findFirst().orElseThrow(); + return reconcilers().findFirst().orElseThrow(); } - @SuppressWarnings({"rawtypes"}) public T getControllerOfType(Class type) { - return operator.getControllers().stream() - .map(Controller::getReconciler) + return reconcilers() .filter(type::isInstance) .map(type::cast) .findFirst() @@ -81,18 +76,7 @@ public T getControllerOfType(Class type) { @SuppressWarnings("unchecked") protected void before(ExtensionContext context) { - LOGGER.info("Initializing integration test in namespace {}", namespace); - - kubernetesClient - .namespaces() - .create(new NamespaceBuilder().withNewMetadata().withName(namespace).endMetadata().build()); - - kubernetesClient - .resourceList(infrastructure) - .createOrReplace(); - kubernetesClient - .resourceList(infrastructure) - .waitUntilReady(infrastructureTimeout.toMillis(), TimeUnit.MILLISECONDS); + super.before(context); for (var ref : reconcilers) { final var config = configurationService.getConfigurationFor(ref.reconciler); @@ -103,6 +87,7 @@ protected void before(ExtensionContext context) { oconfig.withRetry(ref.retry); } + final var kubernetesClient = getKubernetesClient(); try (InputStream is = getClass().getResourceAsStream(path)) { final var crd = kubernetesClient.load(is); crd.createOrReplace(); @@ -116,7 +101,6 @@ protected void before(ExtensionContext context) { ((KubernetesClientAware) ref.reconciler).setKubernetesClient(kubernetesClient); } - this.operator.register(ref.reconciler, oconfig.build()); } @@ -125,22 +109,7 @@ protected void before(ExtensionContext context) { } protected void after(ExtensionContext context) { - if (namespace != null) { - if (preserveNamespaceOnError && context.getExecutionException().isPresent()) { - LOGGER.info("Preserving namespace {}", namespace); - } else { - kubernetesClient.resourceList(infrastructure).delete(); - LOGGER.info("Deleting namespace {} and stopping operator", namespace); - kubernetesClient.namespaces().withName(namespace).delete(); - if (waitForNamespaceDeletion) { - LOGGER.info("Waiting for namespace {} to be deleted", namespace); - Awaitility.await("namespace deleted") - .pollInterval(50, TimeUnit.MILLISECONDS) - .atMost(90, TimeUnit.SECONDS) - .until(() -> kubernetesClient.namespaces().withName(namespace).get() == null); - } - } - } + super.after(context); try { this.operator.stop(); @@ -150,53 +119,14 @@ protected void after(ExtensionContext context) { } @SuppressWarnings("rawtypes") - public static class Builder extends AbstractBuilder { + public static class Builder extends AbstractBuilder { private final List reconcilers; - private ConfigurationService configurationService; protected Builder() { super(); - this.configurationService = new BaseConfigurationService(Version.UNKNOWN); this.reconcilers = new ArrayList<>(); } - public Builder preserveNamespaceOnError(boolean value) { - this.preserveNamespaceOnError = value; - return this; - } - - public Builder waitForNamespaceDeletion(boolean value) { - this.waitForNamespaceDeletion = value; - return this; - } - - public Builder oneNamespacePerClass(boolean value) { - this.oneNamespacePerClass = value; - return this; - } - - public Builder withConfigurationService(ConfigurationService value) { - configurationService = value; - return this; - } - - public Builder withInfrastructureTimeout(Duration value) { - infrastructureTimeout = value; - return this; - } - - public Builder withInfrastructure(List hm) { - infrastructure.addAll(hm); - return this; - } - - public Builder withInfrastructure(HasMetadata... hms) { - for (HasMetadata hm : hms) { - infrastructure.add(hm); - } - return this; - } - @SuppressWarnings("rawtypes") public Builder withReconciler(Reconciler value) { reconcilers.add(new ReconcilerSpec(value, null)); From d38d694e1a7775e1374b8e71c136dd4cb6002283 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 7 Feb 2022 10:16:31 +0000 Subject: [PATCH 0290/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 8e5f9ab481..a2308d86e9 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 04ed9af26c..4262525b45 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 38acf8c29e..89a631f0ad 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 25477d0d2c..0b16c01603 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index f7aaa1b89e..723fd99569 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index bda0efa060..bd73c6390f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 3b3c74f283..933395a4fb 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 4d95bcee4e..21c3fbc405 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 827f6d9b40..4ec7713b93 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index e65be3aefb..6cf1f8d991 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 6f8e9a2d65..8a9f424049 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 0a16d9112b..f56c811cbd 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index c839a902a3..f97e16f347 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.1-SNAPSHOT + 2.1.2-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From e0800b8983e2d1b61b68b78b88fc15e9c6cc51b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Feb 2022 08:54:08 +0100 Subject: [PATCH 0291/1608] chore(deps): bump slf4j-api from 1.7.35 to 1.7.36 (#916) Bumps [slf4j-api](https://github.com/qos-ch/slf4j) from 1.7.35 to 1.7.36. - [Release notes](https://github.com/qos-ch/slf4j/releases) - [Commits](https://github.com/qos-ch/slf4j/compare/v_1.7.35...v_1.7.36) --- updated-dependencies: - dependency-name: org.slf4j:slf4j-api dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 723fd99569..4dc1a81a7c 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 5.8.2 5.12.0 - 1.7.35 + 1.7.36 2.17.1 4.3.1 3.12.0 From dee38193d27fb2c43028bc85172ff6a2cf0e0e68 Mon Sep 17 00:00:00 2001 From: Junfan Zhang Date: Wed, 9 Feb 2022 18:49:20 +0800 Subject: [PATCH 0292/1608] Minor fix: optimize var usage (#917) --- .../operator/sample/WebPageReconciler.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index c252ba6381..e02c438417 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -38,6 +38,8 @@ public UpdateControl reconcile(WebPage webPage, Context context) { } String ns = webPage.getMetadata().getNamespace(); + String configMapName = configMapName(webPage); + String deploymentName = deploymentName(webPage); Map data = new HashMap<>(); data.put("index.html", webPage.getSpec().getHtml()); @@ -46,22 +48,23 @@ public UpdateControl reconcile(WebPage webPage, Context context) { new ConfigMapBuilder() .withMetadata( new ObjectMetaBuilder() - .withName(configMapName(webPage)) + .withName(configMapName) .withNamespace(ns) .build()) .withData(data) .build(); Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); - deployment.getMetadata().setName(deploymentName(webPage)); + deployment.getMetadata().setName(deploymentName); deployment.getMetadata().setNamespace(ns); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName(webPage)); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + deployment .getSpec() .getTemplate() .getMetadata() .getLabels() - .put("app", deploymentName(webPage)); + .put("app", deploymentName); deployment .getSpec() .getTemplate() @@ -69,7 +72,7 @@ public UpdateControl reconcile(WebPage webPage, Context context) { .getVolumes() .get(0) .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + new ConfigMapVolumeSourceBuilder().withName(configMapName).build()); Service service = loadYaml(Service.class, "service.yaml"); service.getMetadata().setName(serviceName(webPage)); From 78a305b48a6e34df6c46a568e02313090cf69f47 Mon Sep 17 00:00:00 2001 From: csviri Date: Wed, 9 Feb 2022 15:58:08 +0100 Subject: [PATCH 0293/1608] fix: merge main to next - fixed conflicts --- sample-operators/mysql-schema/pom.xml | 6 ++++++ .../operator/sample/MySQLSchemaOperatorE2E.java | 4 ++-- sample-operators/tomcat-operator/pom.xml | 6 ++++++ .../javaoperatorsdk/operator/sample/TomcatOperatorE2E.java | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index c8932f902f..ca2d4e8cb5 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -72,6 +72,12 @@ jackson-dataformat-yaml 2.13.1 + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + 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 cf8284f179..6049627f94 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 @@ -65,8 +65,8 @@ boolean isLocal() { @RegisterExtension AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new MySQLSchemaReconciler(client, - new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))) + .withReconciler( + new MySQLSchemaReconciler(new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))) .withInfrastructure(infrastructure) .build() : E2EOperatorExtension.builder() diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index cee6935745..7e0af474c5 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -57,6 +57,12 @@ 4.1.1 test + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index 042f4973cf..aa1a2b81ff 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -44,7 +44,7 @@ boolean isLocal() { AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new TomcatReconciler(client)) + .withReconciler(new TomcatReconciler()) .withReconciler(new WebappReconciler(client)) .build() : E2EOperatorExtension.builder() From d3ad7e50ff040620b32f6e63359b73aee3d11ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 9 Feb 2022 17:34:41 +0100 Subject: [PATCH 0294/1608] fix: run e2e tests on next (#919) --- .github/workflows/e2e-test.yml | 3 ++- .../operator/sample/MySQLSchemaReconciler.java | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 31b89e1b20..774df82b48 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -3,10 +3,11 @@ name: Integration & End to End tests on: pull_request: - branches: [ main ] + branches: [ main, next ] push: branches: - main + - next jobs: sample_operators_tests: diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index ff4fcad131..096f3ed4a7 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -23,9 +23,11 @@ import io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.SecretDependentResource; import io.javaoperatorsdk.operator.sample.schema.Schema; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; import static java.lang.String.format; -@ControllerConfiguration( +// todo handle this, should work with finalizer +@ControllerConfiguration(finalizerName = NO_FINALIZER, dependents = { @Dependent(resourceType = Secret.class, type = SecretDependentResource.class), @Dependent(resourceType = Schema.class, type = SchemaDependentResource.class) From 57dcbc57f9d00df3aff08cbdd217f652311e3296 Mon Sep 17 00:00:00 2001 From: Jose Carvajal Date: Thu, 10 Feb 2022 14:43:44 +0100 Subject: [PATCH 0295/1608] doc: add groupId in Maven query (#921) When querying Maven central using the artifactId `operator-framework`, there were multiple matches: - One using the groupId: `com.github.containersolutions` - Another one using: `io.javaoperatorsdk` While is obvious that the right one is the one using the groupId `io.javaoperatorsdk`, we can avoid confusions by querying Maven central using both the artifactId and the group Id fields. --- docs/documentation/use-samples.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/documentation/use-samples.md b/docs/documentation/use-samples.md index 616a782181..1bc32851a7 100644 --- a/docs/documentation/use-samples.md +++ b/docs/documentation/use-samples.md @@ -26,14 +26,14 @@ examples: # Implementing a Sample Operator -Add [dependency](https://search.maven.org/search?q=a:operator-framework) to your project with Maven: +Add [dependency](https://search.maven.org/search?q=a:operator-framework%20AND%20g:io.javaoperatorsdk) to your project with Maven: ```xml io.javaoperatorsdk operator-framework - {see https://search.maven.org/search?q=a:operator-framework for latest version} + {see https://search.maven.org/search?q=a:operator-framework%20AND%20g:io.javaoperatorsdk for latest version} ``` @@ -188,14 +188,14 @@ public class QuarkusOperator implements QuarkusApplication { You can also let Spring Boot wire your application together and automatically register the controllers. -Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter) to your project: +Add [this dependency](https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk) to your project: ```xml io.javaoperatorsdk operator-framework-spring-boot-starter - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for + {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk for latest version} @@ -224,7 +224,7 @@ necessary, but it doesn't need real access to a Kubernetes cluster. io.javaoperatorsdk operator-framework-spring-boot-starter-test - {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter for + {see https://search.maven.org/search?q=a:operator-framework-spring-boot-starter%20AND%20g:io.javaoperatorsdk for latest version} From 6a05ebc52f683e77238db9ce14765876b7fcc36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Feb 2022 10:01:14 +0100 Subject: [PATCH 0296/1608] Dependent resources standalone mode (#914) --- .../operator/ReconcilerUtils.java | 17 ++ .../dependent/AbstractDependentResource.java | 31 +++ .../dependent/DependentResource.java | 50 +--- .../reconciler/dependent/DesiredSupplier.java | 10 + .../KubernetesDependentResource.java | 150 ++++++++++ .../api/reconciler/dependent/Persister.java | 11 - ...StandaloneKubernetesDependentResource.java | 63 +++++ .../DependentResourceController.java | 108 ++------ .../dependent/DependentResourceManager.java | 25 +- ...KubernetesDependentResourceController.java | 72 ++--- .../operator/ReconcilerUtilsTest.java | 43 +++ .../StandaloneDependentResourceIT.java | 54 ++++ ...formerEventSourceTestCustomReconciler.java | 42 ++- ...StandaloneDependentTestCustomResource.java | 15 + ...loneDependentTestCustomResourceStatus.java | 5 + .../StandaloneDependentTestReconciler.java | 77 ++++++ .../standalonedependent/nginx-deployment.yaml | 21 ++ .../sample/MySQLSchemaReconciler.java | 39 +-- .../sample/SchemaDependentResource.java | 54 ++-- .../sample/SecretDependentResource.java | 54 ++++ .../sample/MySQLSchemaOperatorE2E.java | 6 +- .../sample/DeploymentDependentResource.java | 18 +- .../sample/ServiceDependentResource.java | 14 +- .../sample/TomcatDependentResource.java | 41 --- sample-operators/webpage/pom.xml | 5 + .../operator/sample/WebPageReconciler.java | 256 +++++++++--------- 26 files changed, 821 insertions(+), 460 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java create mode 100644 operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java delete mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index e3a6da1e5a..6f4e19692b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Locale; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -98,4 +100,19 @@ public static String getDefaultReconcilerName(String reconcilerClassName) { } return reconcilerClassName.toLowerCase(Locale.ROOT); } + + public static boolean specsEqual(HasMetadata r1, HasMetadata r2) { + return getSpec(r1).equals(getSpec(r2)); + } + + // will be replaced with: https://github.com/fabric8io/kubernetes-client/issues/3816 + public static Object getSpec(HasMetadata resource) { + try { + Method getSpecMethod = resource.getClass().getMethod("getSpec"); + return getSpecMethod.invoke(resource); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java new file mode 100644 index 0000000000..ea8b8bc06b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -0,0 +1,31 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public abstract class AbstractDependentResource + implements DependentResource { + + @Override + public void reconcile(P primary, Context context) { + var actual = getResource(primary); + var desired = desired(primary, context); + if (actual.isEmpty()) { + create(desired, primary, context); + } else { + if (!match(actual.get(), desired, context)) { + update(actual.get(), desired, primary, context); + } + } + } + + protected abstract R desired(P primary, Context context); + + protected abstract boolean match(R actual, R target, Context context); + + protected abstract R create(R target, P primary, Context context); + + // the actual needed to copy/preserve new labels or annotations + protected abstract R update(R actual, R target, P primary, Context context); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index df144ade1f..b097554107 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -9,54 +9,18 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; public interface DependentResource { - default EventSource initEventSource(EventSourceContext

context) { - throw new IllegalStateException("Must be implemented if not automatically provided by the SDK"); - } + + Optional eventSource(EventSourceContext

context); @SuppressWarnings("unchecked") default Class resourceType() { return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); } - default void delete(R fetched, P primary, Context context) {} - - /** - * Computes the desired state of the dependent based on the state provided by the specified - * primary resource. - * - * The default implementation returns {@code empty} which corresponds to the case where the - * associated dependent should never be created by the associated reconciler or that the global - * state of the cluster doesn't allow for the resource to be created at this point. - * - * @param primary the primary resource associated with the reconciliation process - * @param context the {@link Context} associated with the reconciliation process - * @return an instance of the dependent resource matching the desired state specified by the - * primary resource or {@code empty} if the dependent shouldn't be created at this point - * (or ever) - */ - default Optional desired(P primary, Context context) { - return Optional.empty(); - } + void reconcile(P primary, Context context); + + void delete(P primary, Context context); + + Optional getResource(P primaryResource); - /** - * Checks whether the actual resource as fetched from the cluster matches the desired state - * expressed by the specified primary resource. - * - * The default implementation always return {@code true}, which corresponds to the behavior where - * the dependent never needs to be updated after it's been created. - * - * Note that failure to properly implement this method will lead to infinite loops. In particular, - * for typical Kubernetes resource implementations, simply calling - * {@code desired(primary, context).equals(actual)} is not enough because metadata will usually be - * different. - * - * @param actual the current state of the resource as fetched from the cluster - * @param primary the primary resource associated with the reconciliation request - * @param context the {@link Context} associated with the reconciliation request - * @return {@code true} if the actual state of the resource matches the desired state expressed by - * the specified primary resource, {@code false} otherwise - */ - default boolean match(R actual, P primary, Context context) { - return true; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java new file mode 100644 index 0000000000..b93d75950b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@FunctionalInterface +public interface DesiredSupplier { + + R getDesired(P primary, Context context); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java new file mode 100644 index 0000000000..e8a4a6779e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java @@ -0,0 +1,150 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; + +public abstract class KubernetesDependentResource + extends AbstractDependentResource { + + private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); + + protected KubernetesClient client; + private boolean explicitDelete = false; + private boolean owned = true; + private InformerEventSource informerEventSource; + + public KubernetesDependentResource() { + this(null); + } + + public KubernetesDependentResource(KubernetesClient client) { + this.client = client; + } + + protected void beforeCreateOrUpdate(R desired, P primary) { + if (owned) { + desired.addOwnerReference(primary); + } + } + + @Override + protected boolean match(R actual, R target, Context context) { + return ReconcilerUtils.specsEqual(actual, target); + } + + @SuppressWarnings("unchecked") + @Override + protected R create(R target, P primary, Context context) { + log.debug("Creating target resource with type: " + + "{}, with id: {}", target.getClass(), ResourceID.fromResource(target)); + beforeCreateOrUpdate(target, primary); + Class targetClass = (Class) target.getClass(); + return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) + .create(target); + } + + @SuppressWarnings("unchecked") + @Override + protected R update(R actual, R target, P primary, Context context) { + log.debug("Updating target resource with type: {}, with id: {}", target.getClass(), + ResourceID.fromResource(target)); + beforeCreateOrUpdate(target, primary); + Class targetClass = (Class) target.getClass(); + return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) + .replace(target); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public Optional eventSource(EventSourceContext

context) { + if (informerEventSource != null) { + return Optional.of(informerEventSource); + } + var informerConfig = initInformerConfiguration(context); + informerEventSource = new InformerEventSource(informerConfig, context); + return Optional.of(informerEventSource); + } + + @SuppressWarnings("unchecked") + private InformerConfiguration initInformerConfiguration(EventSourceContext

context) { + PrimaryResourcesRetriever associatedPrimaries = + (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this + : getDefaultPrimaryResourcesRetriever(); + + AssociatedSecondaryResourceIdentifier

associatedSecondary = + (this instanceof AssociatedSecondaryResourceIdentifier) + ? (AssociatedSecondaryResourceIdentifier

) this + : getDefaultAssociatedSecondaryResourceIdentifier(); + + return InformerConfiguration.from(context, resourceType()) + .withPrimaryResourcesRetriever(associatedPrimaries) + .withAssociatedSecondaryResourceIdentifier(associatedSecondary) + .build(); + } + + protected AssociatedSecondaryResourceIdentifier

getDefaultAssociatedSecondaryResourceIdentifier() { + return ResourceID::fromResource; + } + + protected PrimaryResourcesRetriever getDefaultPrimaryResourcesRetriever() { + return Mappers.fromOwnerReference(); + } + + public KubernetesDependentResource setInformerEventSource( + InformerEventSource informerEventSource) { + this.informerEventSource = informerEventSource; + return this; + } + + @Override + public void delete(P primary, Context context) { + if (explicitDelete) { + var resource = getResource(primary); + resource.ifPresent(r -> client.resource(r).delete()); + } + } + + @Override + public Optional getResource(P primaryResource) { + return informerEventSource.getAssociated(primaryResource); + } + + public KubernetesDependentResource setClient(KubernetesClient client) { + this.client = client; + return this; + } + + + public KubernetesDependentResource setExplicitDelete(boolean explicitDelete) { + this.explicitDelete = explicitDelete; + return this; + } + + public boolean isExplicitDelete() { + return explicitDelete; + } + + public boolean isOwned() { + return owned; + } + + public KubernetesDependentResource setOwned(boolean owned) { + this.owned = owned; + return this; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java deleted file mode 100644 index ab34372917..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Persister.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -public interface Persister { - - void createOrReplace(R dependentResource, Context context); - - R getFor(P primary, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java new file mode 100644 index 0000000000..d4d1e00dc2 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java @@ -0,0 +1,63 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; + +// todo shorter name +public class StandaloneKubernetesDependentResource + extends KubernetesDependentResource { + + private final DesiredSupplier desiredSupplier; + private final Class resourceType; + private AssociatedSecondaryResourceIdentifier

associatedSecondaryResourceIdentifier = + ResourceID::fromResource; + private PrimaryResourcesRetriever primaryResourcesRetriever = Mappers.fromOwnerReference(); + + public StandaloneKubernetesDependentResource( + Class resourceType, DesiredSupplier desiredSupplier) { + this(null, resourceType, desiredSupplier); + } + + public StandaloneKubernetesDependentResource( + KubernetesClient client, Class resourceType, DesiredSupplier desiredSupplier) { + super(client); + this.desiredSupplier = desiredSupplier; + this.resourceType = resourceType; + } + + @Override + protected R desired(P primary, Context context) { + return desiredSupplier.getDesired(primary, context); + } + + public Class resourceType() { + return resourceType; + } + + public StandaloneKubernetesDependentResource setAssociatedSecondaryResourceIdentifier( + AssociatedSecondaryResourceIdentifier

associatedSecondaryResourceIdentifier) { + this.associatedSecondaryResourceIdentifier = associatedSecondaryResourceIdentifier; + return this; + } + + public StandaloneKubernetesDependentResource setPrimaryResourcesRetriever( + PrimaryResourcesRetriever primaryResourcesRetriever) { + this.primaryResourcesRetriever = primaryResourcesRetriever; + return this; + } + + @Override + protected AssociatedSecondaryResourceIdentifier

getDefaultAssociatedSecondaryResourceIdentifier() { + return this.associatedSecondaryResourceIdentifier; + } + + @Override + protected PrimaryResourcesRetriever getDefaultPrimaryResourcesRetriever() { + return this.primaryResourcesRetriever; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java index d0004111af..e49a55a73a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java @@ -2,133 +2,65 @@ import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @Ignore -public class DependentResourceController> - implements DependentResource, Persister, Reconciler

{ - - private static final Logger log = LoggerFactory.getLogger(DependentResourceController.class); +public class DependentResourceController, D extends DependentResource> + implements DependentResource { - private final Persister persister; - private final DependentResource delegate; + private final D delegate; private final C configuration; - public DependentResourceController(DependentResource delegate, C configuration) { + public DependentResourceController(D delegate, C configuration) { this.delegate = delegate; - persister = initPersister(delegate); - this.configuration = configuration; + this.configuration = initConfiguration(delegate, configuration); } - @Override - public Class resourceType() { - return delegate.resourceType(); + protected C initConfiguration(D delegate, C configuration) { + // default implementation just returns the specified one + return configuration; } @Override - public boolean match(R actual, P primary, Context context) { - return delegate.match(actual, primary, context); + public Class resourceType() { + return delegate.resourceType(); } @Override - public Optional desired(P primary, Context context) { - return delegate.desired(primary, context); + public void delete(P primary, Context context) { + delegate.delete(primary, context); } @Override - public void delete(R fetched, P primary, Context context) { - delegate.delete(fetched, primary, context); - } - - @SuppressWarnings("unchecked") - protected Persister initPersister(DependentResource delegate) { - if (delegate instanceof Persister) { - return (Persister) delegate; - } else { - throw new IllegalArgumentException( - "DependentResource '" + delegate.getClass().getName() + "' must implement Persister"); - } + public Optional getResource(P primaryResource) { + return delegate.getResource(primaryResource); } - public String descriptionFor(R resource) { - return resource.toString(); - } - - public Class getResourceType() { - return delegate.resourceType(); - } @Override - public EventSource initEventSource(EventSourceContext

context) { - return delegate.initEventSource(context); + public Optional eventSource(EventSourceContext

context) { + return delegate.eventSource(context); } - @Override - public void createOrReplace(R dependentResource, Context context) { - persister.createOrReplace(dependentResource, context); - } - - @Override - public R getFor(P primary, Context context) { - return persister.getFor(primary, context); - } public C getConfiguration() { return configuration; } - @Override - public UpdateControl

reconcile(P resource, Context context) { - var actual = getFor(resource, context); - if (actual == null || !match(actual, resource, context)) { - final var desired = desired(resource, context); - desired.ifPresent(d -> createOrReplaceDependent(resource, d, context)); - } - return UpdateControl.noUpdate(); + protected D delegate() { + return delegate; } @Override - public DeleteControl cleanup(P primary, Context context) { - var dependent = getFor(primary, context); - if (dependent != null) { - delete(dependent, primary, context); - logOperationInfo(primary, dependent, "Deleting"); - } else { - log.info("Ignoring already deleted {} for '{}' {}", - getResourceType().getName(), - primary.getMetadata().getName(), - primary.getKind()); - } - return Reconciler.super.cleanup(primary, context); + public void reconcile(P resource, Context context) { + delegate.reconcile(resource, context); } - protected void createOrReplaceDependent(P primary, R dependent, Context context) { - logOperationInfo(primary, dependent, "Reconciling"); - // commit the changes - // todo: add metrics timing for dependent resource - createOrReplace(dependent, context); - } - - private void logOperationInfo(P resource, R dependentResource, String operationDescription) { - if (log.isInfoEnabled()) { - log.info("{} {} for '{}' {}", operationDescription, - descriptionFor(dependentResource), - resource.getMetadata().getName(), - resource.getKind()); - } - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 8ab04d7d09..ac791526fc 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -5,6 +5,7 @@ import java.util.List; 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.DependentResourceConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; @@ -18,6 +19,7 @@ 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.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -42,9 +44,11 @@ public List prepareEventSources(EventSourceContext

context) { List sources = new ArrayList<>(configured.size() + 5); configured.forEach(dependent -> { - final var dependentResourceController = from(dependent); + final var dependentResourceController = from(dependent, context.getClient()); dependents.add(dependentResourceController); - sources.add(dependentResourceController.initEventSource(context)); + dependentResourceController.eventSource(context) + .ifPresent(es -> sources.add((EventSource) es)); + }); return sources; @@ -68,7 +72,7 @@ public UpdateControl

reconcile(P resource, Context context) { @Override public DeleteControl cleanup(P resource, Context context) { initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.cleanup(resource, context)); + dependents.forEach(dependent -> dependent.delete(resource, context)); return Reconciler.super.cleanup(resource, context); } @@ -80,14 +84,23 @@ private void initContextIfNeeded(P resource, Context context) { } } - private DependentResourceController from(DependentResourceConfiguration config) { + private DependentResourceController from(DependentResourceConfiguration config, + KubernetesClient client) { try { final var dependentResource = (DependentResource) config.getDependentResourceClass().getConstructor() .newInstance(); if (config instanceof KubernetesDependentResourceConfiguration) { - return new KubernetesDependentResourceController(dependentResource, - (KubernetesDependentResourceConfiguration) config); + if (dependentResource instanceof KubernetesDependentResource) { + final var kubeDependentResource = (KubernetesDependentResource) dependentResource; + kubeDependentResource.setClient(client); + return new KubernetesDependentResourceController(kubeDependentResource, + (KubernetesDependentResourceConfiguration) config); + } else { + throw new IllegalArgumentException("A " + + KubernetesDependentResourceConfiguration.class.getCanonicalName() + + " must be associated to a " + KubernetesDependentResource.class.getCanonicalName()); + } } else { return new DependentResourceController(dependentResource, config); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java index 63d533193b..733460ef31 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java @@ -1,14 +1,13 @@ package io.javaoperatorsdk.operator.processing.dependent; +import java.util.Optional; + import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -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.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; @@ -16,17 +15,19 @@ @Ignore public class KubernetesDependentResourceController - extends DependentResourceController> { - - private final KubernetesDependentResourceConfiguration configuration; - private KubernetesClient client; - private InformerEventSource informer; + extends + DependentResourceController, KubernetesDependentResource> { + public KubernetesDependentResourceController(KubernetesDependentResource delegate, + KubernetesDependentResourceConfiguration configuration) { + super(delegate, configuration); + } @SuppressWarnings("unchecked") - public KubernetesDependentResourceController(DependentResource delegate, + @Override + protected KubernetesDependentResourceConfiguration initConfiguration( + KubernetesDependentResource delegate, KubernetesDependentResourceConfiguration configuration) { - super(delegate, configuration); // todo: check if we can validate that types actually match properly final var associatedPrimaries = (delegate instanceof PrimaryResourcesRetriever) @@ -41,50 +42,15 @@ public KubernetesDependentResourceController(DependentResource delegate, .withPrimaryResourcesRetriever(associatedPrimaries) .withAssociatedSecondaryResourceIdentifier(associatedSecondary) .build(); - this.configuration = - KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned(), - configuration.getDependentResourceClass()); - } - - @SuppressWarnings("unchecked") - @Override - protected Persister initPersister(DependentResource delegate) { - return (delegate instanceof Persister) ? (Persister) delegate : this; - } - - @Override - public String descriptionFor(R resource) { - return String.format("'%s' %s dependent in namespace %s", resource.getMetadata().getName(), - resource.getFullResourceName(), - resource.getMetadata().getNamespace()); - } - - @Override - public EventSource initEventSource(EventSourceContext

context) { - this.client = context.getClient(); - informer = new InformerEventSource<>(configuration, context); - return informer; - } - - @Override - public void createOrReplace(R dependentResource, Context context) { - client.resource(dependentResource).createOrReplace(); - } - - @Override - public R getFor(P primary, Context context) { - return informer.getAssociated(primary).orElse(null); - } - - public boolean owned() { - return getConfiguration().isOwned(); + return KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned(), + configuration.getDependentResourceClass()); } @Override - protected void createOrReplaceDependent(P primary, R dependent, Context context) { - if (owned()) { - dependent.addOwnerReference(primary); - } - super.createOrReplaceDependent(primary, dependent, context); + public Optional eventSource(EventSourceContext

context) { + var informer = new InformerEventSource<>(getConfiguration(), context); + // todo have this implemented with nicer abstractions + delegate().setInformerEventSource(informer); + return super.eventSource(context); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index d46936b3d5..be82caa36b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -3,6 +3,10 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.PodTemplateSpec; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -11,6 +15,7 @@ import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultNameFor; import static io.javaoperatorsdk.operator.ReconcilerUtils.getDefaultReconcilerName; import static io.javaoperatorsdk.operator.ReconcilerUtils.isFinalizerValid; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -39,4 +44,42 @@ void defaultFinalizerShouldWork() { void noFinalizerMarkerShouldWork() { assertTrue(isFinalizerValid(Constants.NO_FINALIZER)); } + + @Test + void equalsSpecObject() { + var d1 = createTestDeployment(); + var d2 = createTestDeployment(); + + assertThat(ReconcilerUtils.specsEqual(d1, d2)).isTrue(); + } + + @Test + void equalArbitraryDifferentSpecsOfObjects() { + var d1 = createTestDeployment(); + var d2 = createTestDeployment(); + d2.getSpec().getTemplate().getSpec().setHostname("otherhost"); + + assertThat(ReconcilerUtils.specsEqual(d1, d2)).isFalse(); + } + + @Test + void getsSpecWithReflection() { + Deployment deployment = new Deployment(); + deployment.setSpec(new DeploymentSpec()); + deployment.getSpec().setReplicas(5); + + DeploymentSpec spec = (DeploymentSpec) ReconcilerUtils.getSpec(deployment); + assertThat(spec.getReplicas()).isEqualTo(5); + } + + private Deployment createTestDeployment() { + Deployment deployment = new Deployment(); + deployment.setSpec(new DeploymentSpec()); + deployment.getSpec().setReplicas(5); + PodTemplateSpec podTemplateSpec = new PodTemplateSpec(); + deployment.getSpec().setTemplate(podTemplateSpec); + podTemplateSpec.setSpec(new PodSpec()); + podTemplateSpec.getSpec().setHostname("localhost"); + return deployment; + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java new file mode 100644 index 0000000000..20c0f20b80 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.dependent; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; +import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestReconciler; + +import static org.awaitility.Awaitility.await; + +class StandaloneDependentResourceIT { + + public static final String DEPENDENT_TEST_NAME = "dependent-test1"; + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new StandaloneDependentTestReconciler()) + .build(); + + @Test + void dependentResourceManagesDeployment() { + StandaloneDependentTestCustomResource customResource = + new StandaloneDependentTestCustomResource(); + customResource.setMetadata(new ObjectMeta()); + customResource.getMetadata().setName(DEPENDENT_TEST_NAME); + var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + + await() + .pollInterval(Duration.ofMillis(300)) + .atMost(Duration.ofSeconds(50)) + .until( + () -> { + var deployment = + operator + .getKubernetesClient() + .resources(Deployment.class) + .inNamespace(createdCR.getMetadata().getNamespace()) + .withName(DEPENDENT_TEST_NAME) + .get(); + return deployment != null + && deployment.getStatus() != null + && deployment.getStatus().getReadyReplicas() != null + && deployment.getStatus().getReadyReplicas() > 0; + }); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 33128a6b8c..d61aa7f0f2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -1,23 +1,18 @@ package io.javaoperatorsdk.operator.sample.informereventsource; +import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; -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.DependentResource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; -import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler.ConfigMapDR; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -25,10 +20,10 @@ * Copies the config map value from spec into status. The main purpose is to test and demonstrate * sample usage of InformerEventSource */ -@ControllerConfiguration(finalizerName = NO_FINALIZER, - dependents = @Dependent(resourceType = ConfigMap.class, type = ConfigMapDR.class)) +@ControllerConfiguration(finalizerName = NO_FINALIZER) public class InformerEventSourceTestCustomReconciler - implements Reconciler { + implements Reconciler, + EventSourceInitializer { private static final Logger LOGGER = LoggerFactory.getLogger(InformerEventSourceTestCustomReconciler.class); @@ -39,22 +34,21 @@ public class InformerEventSourceTestCustomReconciler private final AtomicInteger numberOfExecutions = new AtomicInteger(0); - public static class ConfigMapDR - implements DependentResource, - PrimaryResourcesRetriever { - private final PrimaryResourcesRetriever retriever = Mappers.fromAnnotation( - RELATED_RESOURCE_NAME); + @Override + public List prepareEventSources( + EventSourceContext context) { - @Override - public Set associatedPrimaryResources(ConfigMap dependentResource) { - return retriever.associatedPrimaryResources(dependentResource); - } + InformerConfiguration config = + InformerConfiguration.from(context, ConfigMap.class) + .withPrimaryResourcesRetriever(Mappers.fromAnnotation(RELATED_RESOURCE_NAME)) + .build(); + + return List.of(new InformerEventSource<>(config, context)); } @Override public UpdateControl reconcile( - InformerEventSourceTestCustomResource resource, - Context context) { + InformerEventSourceTestCustomResource resource, Context context) { numberOfExecutions.incrementAndGet(); resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java new file mode 100644 index 0000000000..3e6737c83c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +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("sdt") +public class StandaloneDependentTestCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceStatus.java new file mode 100644 index 0000000000..5f12f1ef72 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceStatus.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +public class StandaloneDependentTestCustomResourceStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java new file mode 100644 index 0000000000..695cc30a31 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -0,0 +1,77 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.StandaloneKubernetesDependentResource; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class StandaloneDependentTestReconciler + implements Reconciler, + EventSourceInitializer, + KubernetesClientAware { + + private KubernetesClient kubernetesClient; + + StandaloneKubernetesDependentResource configMapDependent; + + public StandaloneDependentTestReconciler() { + configMapDependent = + new StandaloneKubernetesDependentResource<>(Deployment.class, (primary, context) -> { + Deployment deployment = loadYaml(Deployment.class, "nginx-deployment.yaml"); + deployment.getMetadata().setName(primary.getMetadata().getName()); + deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + return deployment; + }) { + @Override + protected boolean match(Deployment actual, Deployment target, Context context) { + return Objects.equals(actual.getSpec().getReplicas(), target.getSpec().getReplicas()) && + actual.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals( + target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } + }; + } + + @Override + public List prepareEventSources( + EventSourceContext context) { + return List.of(configMapDependent.eventSource(context).get()); + } + + @Override + public UpdateControl reconcile( + StandaloneDependentTestCustomResource resource, Context context) { + configMapDependent.reconcile(resource, context); + return UpdateControl.noUpdate(); + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + configMapDependent.setClient(kubernetesClient); + } + + @Override + public KubernetesClient getKubernetesClient() { + return this.kubernetesClient; + } + + private T loadYaml(Class clazz, String yaml) { + try (InputStream is = getClass().getResourceAsStream(yaml)) { + return Serialization.unmarshal(is, clazz); + } catch (IOException ex) { + throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); + } + } +} diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml new file mode 100644 index 0000000000..cea0f3c9d6 --- /dev/null +++ b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 +kind: Deployment +metadata: + name: "" +spec: + progressDeadlineSeconds: 600 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: "test-dependent" + replicas: 1 + template: + metadata: + labels: + app: "test-dependent" + spec: + containers: + - name: nginx + image: nginx:1.17.0 + ports: + - containerPort: 80 diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 096f3ed4a7..63c37ac3d7 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.sample; -import java.util.Base64; import java.util.Optional; import org.apache.commons.lang3.RandomStringUtils; @@ -8,7 +7,6 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; @@ -19,8 +17,6 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.SecretDependentResource; import io.javaoperatorsdk.operator.sample.schema.Schema; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -36,14 +32,14 @@ public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler, ContextInitializer, EventSourceContextInjector { - private static final String SECRET_FORMAT = "%s-secret"; - private static final String USERNAME_FORMAT = "%s-user"; + static final String SECRET_FORMAT = "%s-secret"; + static final String USERNAME_FORMAT = "%s-user"; - protected static final String MYSQL_SECRET_NAME = "mysql.secret.name"; - protected static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; - protected static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; - protected static final String MYSQL_DB_CONFIG = "mysql.db.config"; - protected static final String BUILT_SCHEMA = "built schema"; + static final String MYSQL_SECRET_NAME = "mysql.secret.name"; + static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; + static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; + static final String MYSQL_DB_CONFIG = "mysql.db.config"; + static final String BUILT_SCHEMA = "built schema"; static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class); private final MySQLDbConfig mysqlDbConfig; @@ -52,27 +48,6 @@ public MySQLSchemaReconciler(MySQLDbConfig mysqlDbConfig) { this.mysqlDbConfig = mysqlDbConfig; } - public static class SecretDependentResource implements DependentResource { - - private static String encode(String value) { - return Base64.getEncoder().encodeToString(value.getBytes()); - } - - @Override - public Optional desired(MySQLSchema schema, Context context) { - return Optional.of(new SecretBuilder() - .withNewMetadata() - .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) - .withNamespace(schema.getMetadata().getNamespace()) - .endMetadata() - .addToData("MYSQL_USERNAME", encode( - context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) - .addToData("MYSQL_PASSWORD", encode( - context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) - .build()); - } - } - @SuppressWarnings("rawtypes") @Override public void injectInto(EventSourceContext context) { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 8ba9612029..957f0b34ff 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -7,8 +7,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Persister; +import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; import io.javaoperatorsdk.operator.sample.schema.Schema; @@ -16,39 +15,53 @@ import static java.lang.String.format; -public class SchemaDependentResource - implements DependentResource, Persister { +public class SchemaDependentResource extends AbstractDependentResource { private static final int POLL_PERIOD = 500; private MySQLDbConfig dbConfig; @Override - public EventSource initEventSource(EventSourceContext context) { + public Optional eventSource(EventSourceContext context) { dbConfig = context.getMandatory(MySQLSchemaReconciler.MYSQL_DB_CONFIG, MySQLDbConfig.class); - return new PerResourcePollingEventSource<>( + return Optional.of(new PerResourcePollingEventSource<>( new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), POLL_PERIOD, - Schema.class); + Schema.class)); } @Override - public Optional desired(MySQLSchema primary, Context context) { + public Schema desired(MySQLSchema primary, Context context) { + return new Schema(primary.getMetadata().getName(), primary.getSpec().getEncoding()); + } + + @Override + protected boolean match(Schema actual, Schema target, Context context) { + return actual.equals(target); + } + + @Override + protected Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { final var schema = SchemaService.createSchemaAndRelatedUser( connection, - primary.getMetadata().getName(), - primary.getSpec().getEncoding(), + target.getName(), + target.getCharacterSet(), context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_USERNAME, String.class), context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_PASSWORD, String.class)); // put the newly built schema in the context to let the reconciler know we just built it context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); - return Optional.of(schema); + return schema; } catch (SQLException e) { MySQLSchemaReconciler.log.error("Error while creating Schema", e); throw new IllegalStateException(e); } } + @Override + protected Schema update(Schema actual, Schema target, MySQLSchema mySQLSchema, Context context) { + throw new IllegalStateException("Target schema should not be changed: " + mySQLSchema); + } + private Connection getConnection() throws SQLException { String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort()); @@ -58,7 +71,7 @@ private Connection getConnection() throws SQLException { } @Override - public void delete(Schema fetched, MySQLSchema primary, Context context) { + public void delete(MySQLSchema primary, Context context) { try (Connection connection = getConnection()) { var userName = primary.getStatus() != null ? primary.getStatus().getUserName() : null; SchemaService.deleteSchemaAndRelatedUser(connection, primary.getMetadata().getName(), @@ -68,19 +81,16 @@ public void delete(Schema fetched, MySQLSchema primary, Context context) { } } + // todo this should read the resource from event source? @Override - public void createOrReplace(Schema dependentResource, Context context) { - // this is actually implemented in buildFor, the cleaner way to do this would be to have all - // the needed information in Schema instead of creating both the schema and user from - // heterogeneous information - } - - @Override - public Schema getFor(MySQLSchema primary, Context context) { + public Optional getResource(MySQLSchema primaryResource) { try (Connection connection = getConnection()) { - return SchemaService.getSchema(connection, primary.getMetadata().getName()).orElse(null); + var schema = + SchemaService.getSchema(connection, primaryResource.getMetadata().getName()).orElse(null); + return Optional.ofNullable(schema); } catch (SQLException e) { - throw new RuntimeException("Error while trying to delete Schema", e); + throw new RuntimeException("Error while trying read Schema", e); } } + } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java new file mode 100644 index 0000000000..4f6bd1759c --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.Base64; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; + +import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*; + +public class SecretDependentResource extends KubernetesDependentResource + implements AssociatedSecondaryResourceIdentifier { + + private static String encode(String value) { + return Base64.getEncoder().encodeToString(value.getBytes()); + } + + @Override + public Secret desired(MySQLSchema schema, Context context) { + return new SecretBuilder() + .withNewMetadata() + .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) + .withNamespace(schema.getMetadata().getNamespace()) + .endMetadata() + .addToData( + "MYSQL_USERNAME", encode(context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) + .addToData( + "MYSQL_PASSWORD", encode(context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) + .build(); + } + + // An alternative would be to override reconcile() method and exclude the update part. + @Override + protected Secret update(Secret actual, Secret target, MySQLSchema primary, Context context) { + throw new IllegalStateException( + "Secret should not be updated. Secret: " + target + " for custom resource: " + + primary); + } + + @Override + protected boolean match(Secret actual, Secret target, Context context) { + return ResourceID.fromResource(actual).equals(ResourceID.fromResource(target)); + } + + @Override + public ResourceID associatedSecondaryID(MySQLSchema primary) { + return new ResourceID( + String.format(SECRET_FORMAT, primary.getMetadata().getName()), + primary.getMetadata().getNamespace()); + } +} 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 6049627f94..0c1be5efc0 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 @@ -29,7 +29,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; -public class MySQLSchemaOperatorE2E { +class MySQLSchemaOperatorE2E { final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); @@ -113,8 +113,8 @@ public void test() throws IOException { log.info("Creating test MySQLSchema object: {}", testSchema); client.resource(testSchema).createOrReplace(); - log.info("Waiting 5 minutes for expected resources to be created and updated"); - await().atMost(1, MINUTES).ignoreExceptions().untilAsserted(() -> { + log.info("Waiting 2 minutes for expected resources to be created and updated"); + await().atMost(2, MINUTES).ignoreExceptions().untilAsserted(() -> { MySQLSchema updatedSchema = client.resources(MySQLSchema.class).inNamespace(operator.getNamespace()) .withName(testSchema.getMetadata().getName()).get(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 58200df988..b681ccf6b6 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -1,20 +1,20 @@ package io.javaoperatorsdk.operator.sample; -import java.util.Optional; - import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; @KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") public class DeploymentDependentResource - implements DependentResource { + extends KubernetesDependentResource { + + public DeploymentDependentResource() {} @Override - public Optional desired(Tomcat tomcat, Context context) { + public Deployment desired(Tomcat tomcat, Context context) { Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); @@ -39,7 +39,7 @@ public Optional desired(Tomcat tomcat, Context context) { .endTemplate() .endSpec() .build(); - return Optional.of(deployment); + return deployment; } private String tomcatImage(Tomcat tomcat) { @@ -47,8 +47,8 @@ private String tomcatImage(Tomcat tomcat) { } @Override - public boolean match(Deployment fetched, Tomcat tomcat, Context context) { - return fetched.getSpec().getTemplate().getSpec().getContainers().stream() - .findFirst().map(c -> tomcatImage(tomcat).equals(c.getImage())).orElse(false); + public boolean match(Deployment fetched, Deployment target, Context context) { + return fetched.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals(target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 52d043203b..16a6aaff2e 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -1,19 +1,19 @@ package io.javaoperatorsdk.operator.sample; -import java.util.Optional; - import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; + +public class ServiceDependentResource extends KubernetesDependentResource { -public class ServiceDependentResource implements DependentResource { + public ServiceDependentResource() {} @Override - public Optional desired(Tomcat tomcat, Context context) { + public Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); - return Optional.of(new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) + return new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) .editMetadata() .withName(tomcatMetadata.getName()) .withNamespace(tomcatMetadata.getNamespace()) @@ -21,6 +21,6 @@ public Optional desired(Tomcat tomcat, Context context) { .editSpec() .addToSelector("app", tomcatMetadata.getName()) .endSpec() - .build()); + .build(); } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java deleted file mode 100644 index abb4e62358..0000000000 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatDependentResource.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -import java.util.Set; -import java.util.stream.Collectors; - -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; - -public class TomcatDependentResource - implements DependentResource, PrimaryResourcesRetriever, - AssociatedSecondaryResourceIdentifier, EventSourceContextAware { - - private ResourceCache primaryCache; - - @Override - public void initWith(EventSourceContext context) { - this.primaryCache = context.getPrimaryCache(); - } - - @Override - public Set associatedPrimaryResources(Tomcat t) { - // To create an event to a related WebApp resource and trigger the reconciliation - // we need to find which WebApp this Tomcat custom resource is related to. - // To find the related customResourceId of the WebApp resource we traverse the cache to - // and identify it based on naming convention. - return primaryCache - .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) - .map(ResourceID::fromResource) - .collect(Collectors.toSet()); - } - - @Override - public ResourceID associatedSecondaryID(Webapp primary) { - return new ResourceID(primary.getSpec().getTomcat(), primary.getMetadata().getNamespace()); - } -} diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 566ec8b314..40101a7413 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -40,6 +40,11 @@ crd-generator-apt provided + + org.awaitility + awaitility + compile + diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index e02c438417..28d6ed3037 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -2,9 +2,8 @@ import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.time.Duration; +import java.util.*; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -13,22 +12,40 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.Resource; -import io.fabric8.kubernetes.client.dsl.RollableScalableResource; -import io.fabric8.kubernetes.client.dsl.ServiceResource; import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.StandaloneKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; -@ControllerConfiguration -public class WebPageReconciler implements Reconciler, ErrorStatusHandler { +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; +import static org.awaitility.Awaitility.await; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class WebPageReconciler + implements Reconciler, ErrorStatusHandler, EventSourceInitializer { private final Logger log = LoggerFactory.getLogger(getClass()); private final KubernetesClient kubernetesClient; + private StandaloneKubernetesDependentResource configMapDR; + private StandaloneKubernetesDependentResource deploymentDR; + private StandaloneKubernetesDependentResource serviceDR; + public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + List eventSources = new ArrayList<>(3); + configMapDR.eventSource(context).ifPresent(es -> eventSources.add(es)); + deploymentDR.eventSource(context).ifPresent(es -> eventSources.add(es)); + serviceDR.eventSource(context).ifPresent(es -> eventSources.add(es)); + return eventSources; } @Override @@ -37,81 +54,14 @@ public UpdateControl reconcile(WebPage webPage, Context context) { throw new ErrorSimulationException("Simulating error"); } - String ns = webPage.getMetadata().getNamespace(); - String configMapName = configMapName(webPage); - String deploymentName = deploymentName(webPage); - - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - - ConfigMap htmlConfigMap = - new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(configMapName) - .withNamespace(ns) - .build()) - .withData(data) - .build(); - - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(ns); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName).build()); - - Service service = loadYaml(Service.class, "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(ns); - service.getSpec().setSelector(deployment.getSpec().getTemplate().getMetadata().getLabels()); - - ConfigMap existingConfigMap = - kubernetesClient - .configMaps() - .inNamespace(htmlConfigMap.getMetadata().getNamespace()) - .withName(htmlConfigMap.getMetadata().getName()) - .get(); - - log.info("Creating or updating ConfigMap {} in {}", htmlConfigMap.getMetadata().getName(), ns); - kubernetesClient.configMaps().inNamespace(ns).createOrReplace(htmlConfigMap); - log.info("Creating or updating Deployment {} in {}", deployment.getMetadata().getName(), ns); - kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(deployment); - - if (kubernetesClient.services().inNamespace(ns).withName(service.getMetadata().getName()) - .get() == null) { - log.info("Creating Service {} in {}", service.getMetadata().getName(), ns); - kubernetesClient.services().inNamespace(ns).createOrReplace(service); - } - - if (existingConfigMap != null) { - if (!StringUtils.equals( - existingConfigMap.getData().get("index.html"), - htmlConfigMap.getData().get("index.html"))) { - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(webPage)) - .delete(); - } - } + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); WebPageStatus status = new WebPageStatus(); - status.setHtmlConfigMap(htmlConfigMap.getMetadata().getName()); + + waitUntilConfigMapAvailable(webPage); + status.setHtmlConfigMap(configMapDR.getResource(webPage).get().getMetadata().getName()); status.setAreWeGood("Yes!"); status.setErrorMessage(null); webPage.setStatus(status); @@ -119,41 +69,112 @@ public UpdateControl reconcile(WebPage webPage, Context context) { return UpdateControl.updateStatus(webPage); } - @Override - public DeleteControl cleanup(WebPage nginx, Context context) { - log.info("Cleaning up for: {}", nginx.getMetadata().getName()); - - log.info("Deleting ConfigMap {}", configMapName(nginx)); - Resource configMap = - kubernetesClient - .configMaps() - .inNamespace(nginx.getMetadata().getNamespace()) - .withName(configMapName(nginx)); - if (configMap.get() != null) { - configMap.delete(); - } + // todo after implemented we can remove this method: + // https://github.com/java-operator-sdk/java-operator-sdk/issues/924 + private void waitUntilConfigMapAvailable(WebPage webPage) { + await().atMost(Duration.ofSeconds(5)).until(() -> configMapDR.getResource(webPage).isPresent()); + } - log.info("Deleting Deployment {}", deploymentName(nginx)); - RollableScalableResource deployment = - kubernetesClient - .apps() - .deployments() - .inNamespace(nginx.getMetadata().getNamespace()) - .withName(deploymentName(nginx)); - if (deployment.get() != null) { - deployment.cascading(true).delete(); - } + @Override + public Optional updateErrorStatus( + WebPage resource, RetryInfo retryInfo, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return Optional.of(resource); + } - log.info("Deleting Service {}", serviceName(nginx)); - ServiceResource service = - kubernetesClient - .services() - .inNamespace(nginx.getMetadata().getNamespace()) - .withName(serviceName(nginx)); - if (service.get() != null) { - service.delete(); - } - return DeleteControl.defaultDelete(); + private void createDependentResources(KubernetesClient client) { + this.configMapDR = + new StandaloneKubernetesDependentResource<>( + client, + ConfigMap.class, + (WebPage webPage, Context context) -> { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + }) { + @Override + protected boolean match(ConfigMap actual, ConfigMap target, Context context) { + return StringUtils.equals( + actual.getData().get("index.html"), target.getData().get("index.html")); + } + + @Override + protected ConfigMap update( + ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + var cm = super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + return cm; + } + }; + configMapDR.setAssociatedSecondaryResourceIdentifier( + primary -> new ResourceID(configMapName(primary), primary.getMetadata().getNamespace())); + + this.deploymentDR = + new StandaloneKubernetesDependentResource<>( + client, + Deployment.class, + (webPage, context) -> { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; + }) { + @Override + protected boolean match(Deployment actual, Deployment target, Context context) { + // todo comparator + return true; + } + }; + + this.serviceDR = + new StandaloneKubernetesDependentResource<>( + client, + Service.class, + (webPage, context) -> { + Service service = loadYaml(Service.class, "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + }) { + + protected boolean match(Service actual, Service target, Context context) { + // todo comparator + return true; + } + }; } private static String configMapName(WebPage nginx) { @@ -175,11 +196,4 @@ private T loadYaml(Class clazz, String yaml) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); } } - - @Override - public Optional updateErrorStatus(WebPage resource, RetryInfo retryInfo, - RuntimeException e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); - } } From 4e50fc1b086fa73ef19bf0667fe65f93f5eb3727 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 08:55:22 +0100 Subject: [PATCH 0297/1608] chore(deps): bump nexus-staging-maven-plugin from 1.6.8 to 1.6.10 (#930) Bumps nexus-staging-maven-plugin from 1.6.8 to 1.6.10. --- updated-dependencies: - dependency-name: org.sonatype.plugins:nexus-staging-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4dc1a81a7c..920686da5c 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 3.2.2 3.1.0 3.0.1 - 1.6.8 + 1.6.10 2.8.2 2.5.2 5.0.0 From 05fdf02264398b571dc1e94a9d199edb7e7c7049 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 10:13:53 +0100 Subject: [PATCH 0298/1608] chore(deps): bump maven-javadoc-plugin from 3.3.1 to 3.3.2 (#929) Bumps [maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.3.1...maven-javadoc-plugin-3.3.2) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 920686da5c..dfb5330fc5 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ 2.11 3.9.0 3.0.0-M5 - 3.3.1 + 3.3.2 3.2.0 3.2.1 3.2.2 From 088cebe0f996406509ca2d1e8dcfaa5b2fafb3d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Feb 2022 08:17:42 +0100 Subject: [PATCH 0299/1608] chore(deps): bump maven-compiler-plugin from 3.9.0 to 3.10.0 (#933) Bumps [maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.9.0 to 3.10.0. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.9.0...maven-compiler-plugin-3.10.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index dfb5330fc5..0719d0fb6e 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 1.8.2 2.11 - 3.9.0 + 3.10.0 3.0.0-M5 3.3.2 3.2.0 diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index bd73c6390f..7d25dc2f5f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -105,7 +105,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.9.0 + 3.10.0 diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 21c3fbc405..4381948214 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -90,7 +90,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.9.0 + 3.10.0 diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 4ec7713b93..e79e07ac4f 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -60,7 +60,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.9.0 + 3.10.0 From b1fa1e7a98984beb4c6b6c37cf1d54f76c001721 Mon Sep 17 00:00:00 2001 From: Junfan Zhang Date: Tue, 15 Feb 2022 15:19:01 +0800 Subject: [PATCH 0300/1608] Fix the resources type in the webpage example (#934) --- sample-operators/webpage/k8s/operator.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample-operators/webpage/k8s/operator.yaml b/sample-operators/webpage/k8s/operator.yaml index 926b2c31e2..cfdb1fe47c 100644 --- a/sample-operators/webpage/k8s/operator.yaml +++ b/sample-operators/webpage/k8s/operator.yaml @@ -92,7 +92,7 @@ rules: - apiGroups: - "sample.javaoperatorsdk" resources: - - webservers - - webservers/status + - webpages + - webpages/status verbs: - '*' From 110e64b19f09b4cee4f69b9c6f5f6c061b35629f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 15 Feb 2022 11:54:10 +0100 Subject: [PATCH 0301/1608] fix: logging error level and stack trace for k8s client (#936) --- .../processing/event/ReconciliationDispatcher.java | 8 -------- 1 file changed, 8 deletions(-) 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 02c8f8cbb3..14ba12636a 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 @@ -6,7 +6,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.CustomResource; -import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; @@ -49,13 +48,6 @@ public ReconciliationDispatcher(Controller controller) { public PostExecutionControl handleExecution(ExecutionScope executionScope) { try { return handleDispatch(executionScope); - } catch (KubernetesClientException e) { - log.info( - "Kubernetes exception {} {} during event processing, {} failed", - e.getCode(), - e.getMessage(), - executionScope); - return PostExecutionControl.exceptionDuringExecution(e); } catch (RuntimeException e) { log.error("Error during event processing {} failed.", executionScope, e); return PostExecutionControl.exceptionDuringExecution(e); From 323372a2e7b5b05f25a3af1162b420d5d95c19e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Feb 2022 08:24:35 +0100 Subject: [PATCH 0302/1608] chore(deps): bump nexus-staging-maven-plugin from 1.6.10 to 1.6.11 (#941) Bumps nexus-staging-maven-plugin from 1.6.10 to 1.6.11. --- updated-dependencies: - dependency-name: org.sonatype.plugins:nexus-staging-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0719d0fb6e..fac0ce7e01 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 3.2.2 3.1.0 3.0.1 - 1.6.10 + 1.6.11 2.8.2 2.5.2 5.0.0 From 983ce46334e26babb8d53d83f719fa550b8b0c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Feb 2022 08:57:27 +0100 Subject: [PATCH 0303/1608] Configuration polish (#926) Co-authored-by: Chris Laprun --- .../api/config/ControllerConfiguration.java | 4 +- .../ControllerConfigurationOverrider.java | 43 ++++- .../DefaultControllerConfiguration.java | 8 +- .../operator/api/config/Utils.java | 11 ++ .../api/config/dependent/Dependent.java | 10 - .../DependentResourceConfiguration.java | 11 -- .../dependent/DependentResourceSpec.java | 29 +++ ...ernetesDependentResourceConfiguration.java | 66 ------- .../informer/InformerConfiguration.java | 24 +-- .../reconciler/ControllerConfiguration.java | 25 ++- .../EventSourceContextInjector.java | 5 - .../dependent/AbstractDependentResource.java | 4 +- .../api/reconciler/dependent/Dependent.java | 6 + .../dependent/DependentResource.java | 9 +- .../reconciler/dependent/DesiredSupplier.java | 10 - .../dependent/KubernetesClientAware.java | 7 + ...StandaloneKubernetesDependentResource.java | 63 ------ .../operator/processing/Controller.java | 1 - .../DependentResourceController.java | 66 ------- .../dependent/DependentResourceManager.java | 91 ++++----- ...KubernetesDependentResourceController.java | 56 ------ .../kubernetes}/KubernetesDependent.java | 13 +- .../KubernetesDependentResource.java | 121 ++++++------ .../KubernetesDependentResourceConfig.java | 62 ++++++ .../event/source/EventSourceContextAware.java | 8 - .../source/informer/InformerEventSource.java | 22 +-- .../informer/ManagedInformerEventSource.java | 1 - .../operator/api/config/UtilsTest.java | 30 +++ .../operator/junit/OperatorExtension.java | 33 +++- ...=> AnnotationControllerConfiguration.java} | 129 ++++++------- .../runtime/DefaultConfigurationService.java | 2 +- .../StandaloneDependentTestReconciler.java | 44 +++-- .../operator/sample/MySQLSchemaOperator.java | 15 +- .../sample/MySQLSchemaReconciler.java | 24 +-- .../operator/sample/ResourcePollerConfig.java | 21 ++ .../sample/SchemaDependentResource.java | 14 +- .../sample/SecretDependentResource.java | 20 +- .../sample/MySQLSchemaOperatorE2E.java | 115 +++++------ .../sample/DeploymentDependentResource.java | 21 +- .../sample/ServiceDependentResource.java | 7 +- .../operator/sample/TomcatReconciler.java | 7 +- .../operator/sample/WebPageReconciler.java | 181 ++++++++++-------- 42 files changed, 671 insertions(+), 768 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/config/dependent => processing/dependent/kubernetes}/KubernetesDependent.java (73%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler/dependent => processing/dependent/kubernetes}/KubernetesDependentResource.java (55%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java rename operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/{AnnotationConfiguration.java => AnnotationControllerConfiguration.java} (50%) create mode 100644 sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java 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 c077a7876d..40b32043af 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 @@ -7,7 +7,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -50,7 +50,7 @@ default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } - default List getDependentResources() { + default List getDependentResources() { return Collections.emptyList(); } 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 b21ca22b46..141b58ded0 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 @@ -3,9 +3,12 @@ import java.time.Duration; import java.util.HashSet; 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.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class ControllerConfigurationOverrider { @@ -18,6 +21,7 @@ public class ControllerConfigurationOverrider { private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private Duration reconciliationMaxInterval; + private List dependentResourceSpecs; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizer(); @@ -27,8 +31,8 @@ private ControllerConfigurationOverrider(ControllerConfiguration original) { labelSelector = original.getLabelSelector(); customResourcePredicate = original.getEventFilter(); reconciliationMaxInterval = original.reconciliationMaxInterval().orElse(null); + dependentResourceSpecs = original.getDependentResources(); this.original = original; - } public ControllerConfigurationOverrider withFinalizer(String finalizer) { @@ -84,6 +88,41 @@ public ControllerConfigurationOverrider withReconciliationMaxInterval( return this; } + /** + * If a {@link DependentResourceSpec} already exists with the same dependentResourceClass it will + * be replaced. Otherwise, an exception is thrown; + * + * @param dependentResourceSpec to add or replace + */ + public void replaceDependentResourceConfig(DependentResourceSpec dependentResourceSpec) { + var currentConfig = + findConfigForDependentResourceClass(dependentResourceSpec.getDependentResourceClass()); + if (currentConfig.isEmpty()) { + throw new IllegalStateException("Cannot find DependentResource config for class: " + + dependentResourceSpec.getDependentResourceClass()); + } + dependentResourceSpecs.remove(currentConfig.get()); + dependentResourceSpecs.add(dependentResourceSpec); + } + + public void addNewDependentResourceConfig(DependentResourceSpec dependentResourceSpec) { + var currentConfig = + findConfigForDependentResourceClass(dependentResourceSpec.getDependentResourceClass()); + if (currentConfig.isPresent()) { + throw new IllegalStateException( + "Config already present for class: " + + dependentResourceSpec.getDependentResourceClass()); + } + dependentResourceSpecs.add(dependentResourceSpec); + } + + private Optional findConfigForDependentResourceClass( + Class dependentResourceClass) { + return dependentResourceSpecs.stream() + .filter(dc -> dc.getDependentResourceClass().equals(dependentResourceClass)) + .findFirst(); + } + public ControllerConfiguration build() { return new DefaultControllerConfiguration<>( original.getAssociatedReconcilerClassName(), @@ -98,7 +137,7 @@ public ControllerConfiguration build() { original.getResourceClass(), reconciliationMaxInterval, original.getConfigurationService(), - original.getDependentResources()); + dependentResourceSpecs); } 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 index 188311cfdb..8ce59dd01e 100644 --- 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 @@ -7,7 +7,7 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class DefaultControllerConfiguration @@ -21,7 +21,7 @@ public class DefaultControllerConfiguration private final boolean generationAware; private final RetryConfiguration retryConfiguration; private final ResourceEventFilter resourceEventFilter; - private final List dependents; + private final List dependents; private final Duration reconciliationMaxInterval; // NOSONAR constructor is meant to provide all information @@ -38,7 +38,7 @@ public DefaultControllerConfiguration( Class resourceClass, Duration reconciliationMaxInterval, ConfigurationService service, - List dependents) { + List dependents) { super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -102,7 +102,7 @@ public ResourceEventFilter getEventFilter() { } @Override - public List getDependentResources() { + public List getDependentResources() { return dependents; } 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 3944cd3ecc..8d9990ee9c 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 @@ -2,10 +2,12 @@ import java.io.IOException; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; import java.util.Properties; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,8 +71,17 @@ public static boolean debugThreadPool() { return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); } + public static Class getFirstTypeArgumentFromExtendedClass(Class clazz) { + Type type = clazz.getGenericSuperclass(); + return (Class) ((ParameterizedType) type).getActualTypeArguments()[0]; + } + public static Class getFirstTypeArgumentFromInterface(Class clazz) { ParameterizedType type = (ParameterizedType) clazz.getGenericInterfaces()[0]; return (Class) type.getActualTypeArguments()[0]; } + + public static T valueOrDefault(C annotation, Function mapper, T defaultValue) { + return annotation == null ? defaultValue : mapper.apply(annotation); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java deleted file mode 100644 index 017f4363a6..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/Dependent.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.javaoperatorsdk.operator.api.config.dependent; - -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; - -public @interface Dependent { - - Class resourceType(); - - Class type(); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java deleted file mode 100644 index c0f428c12d..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfiguration.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.api.config.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; - -public interface DependentResourceConfiguration { - - Class> getDependentResourceClass(); - - Class getResourceClass(); -} 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 new file mode 100644 index 0000000000..aea90b5e36 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.api.config.dependent; + +import java.util.Optional; + +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; + +public class DependentResourceSpec, C> { + + private final Class dependentResourceClass; + + private final C dependentResourceConfig; + + public DependentResourceSpec(Class dependentResourceClass) { + this(dependentResourceClass, null); + } + + public DependentResourceSpec(Class dependentResourceClass, C dependentResourceConfig) { + this.dependentResourceClass = dependentResourceClass; + this.dependentResourceConfig = dependentResourceConfig; + } + + public Class getDependentResourceClass() { + return dependentResourceClass; + } + + public Optional getDependentResourceConfiguration() { + return Optional.ofNullable(dependentResourceConfig); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java deleted file mode 100644 index 6872322e4f..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependentResourceConfiguration.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.javaoperatorsdk.operator.api.config.dependent; - -import java.util.Set; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; - -public interface KubernetesDependentResourceConfiguration - extends InformerConfiguration, DependentResourceConfiguration { - - class DefaultKubernetesDependentResourceConfiguration - extends DefaultInformerConfiguration - implements KubernetesDependentResourceConfiguration { - - private final boolean owned; - private final Class> dependentResourceClass; - - protected DefaultKubernetesDependentResourceConfiguration( - ConfigurationService service, - String labelSelector, Class resourceClass, - PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, - AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange, Set namespaces, boolean owned, - Class> dependentResourceClass) { - super(service, labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, - skipUpdateEventPropagationIfNoChange, namespaces); - this.owned = owned; - this.dependentResourceClass = dependentResourceClass; - } - - public boolean isOwned() { - return owned; - } - - @Override - public Class> getDependentResourceClass() { - return dependentResourceClass; - } - - @Override - public Class getResourceClass() { - return super.getResourceClass(); - } - } - - static KubernetesDependentResourceConfiguration from( - InformerConfiguration cfg, boolean owned, - Class dependentResourceClass) { - return new DefaultKubernetesDependentResourceConfiguration(cfg.getConfigurationService(), - cfg.getLabelSelector(), cfg.getResourceClass(), cfg.getPrimaryResourcesRetriever(), - cfg.getAssociatedResourceIdentifier(), cfg.isSkipUpdateEventPropagationIfNoChange(), - cfg.getNamespaces(), owned, - (Class>) dependentResourceClass); - } - - boolean isOwned(); - - @Override - default Class getResourceClass() { - return InformerConfiguration.super.getResourceClass(); - } -} 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 c4d747a990..acb5bd23c4 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 @@ -22,13 +22,12 @@ class DefaultInformerConfiguration private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; private final AssociatedSecondaryResourceIdentifier

associatedWith; - private final boolean skipUpdateEventPropagationIfNoChange; protected DefaultInformerConfiguration(ConfigurationService service, String labelSelector, Class resourceClass, PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, AssociatedSecondaryResourceIdentifier

associatedWith, - boolean skipUpdateEventPropagationIfNoChange, Set namespaces) { + Set namespaces) { super(labelSelector, resourceClass, namespaces); setConfigurationService(service); this.secondaryToPrimaryResourcesIdSet = @@ -36,7 +35,6 @@ protected DefaultInformerConfiguration(ConfigurationService service, String labe Mappers.fromOwnerReference()); this.associatedWith = Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); - this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; } public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { @@ -47,22 +45,16 @@ public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier( return associatedWith; } - public boolean isSkipUpdateEventPropagationIfNoChange() { - return skipUpdateEventPropagationIfNoChange; - } } PrimaryResourcesRetriever getPrimaryResourcesRetriever(); AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier(); - boolean isSkipUpdateEventPropagationIfNoChange(); - class InformerConfigurationBuilder { private PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; private AssociatedSecondaryResourceIdentifier

associatedWith; - private boolean skipUpdateEventPropagationIfNoChange = true; private Set namespaces; private String labelSelector; private final Class resourceClass; @@ -86,16 +78,6 @@ public InformerConfigurationBuilder withAssociatedSecondaryResourceIdentif return this; } - public InformerConfigurationBuilder withoutSkippingEventPropagationIfUnchanged() { - this.skipUpdateEventPropagationIfNoChange = false; - return this; - } - - public InformerConfigurationBuilder skippingEventPropagationIfUnchanged( - boolean skipIfUnchanged) { - this.skipUpdateEventPropagationIfNoChange = skipIfUnchanged; - return this; - } public InformerConfigurationBuilder withNamespaces(String... namespaces) { this.namespaces = namespaces != null ? Set.of(namespaces) : Collections.emptySet(); @@ -115,7 +97,7 @@ public InformerConfigurationBuilder withLabelSelector(String labelSelector public InformerConfiguration build() { return new DefaultInformerConfiguration<>(configurationService, labelSelector, resourceClass, - secondaryToPrimaryResourcesIdSet, associatedWith, skipUpdateEventPropagationIfNoChange, + secondaryToPrimaryResourcesIdSet, associatedWith, namespaces); } } @@ -136,8 +118,6 @@ static InformerConfigurationBuild configuration.getConfigurationService()) .withNamespaces(configuration.getNamespaces()) .withLabelSelector(configuration.getLabelSelector()) - .skippingEventPropagationIfUnchanged( - configuration.isSkipUpdateEventPropagationIfNoChange()) .withAssociatedSecondaryResourceIdentifier( configuration.getAssociatedResourceIdentifier()) .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); 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 85eed38025..7d4c1fd0e8 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 @@ -4,10 +4,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.concurrent.TimeUnit; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; -import io.javaoperatorsdk.operator.processing.dependent.DependentResourceController; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; @Retention(RetentionPolicy.RUNTIME) @@ -51,26 +49,27 @@ */ String labelSelector() default Constants.EMPTY_STRING; - /** * Optional list of classes providing custom {@link ResourceEventFilter}. * * @return the list of event filters. */ - @SuppressWarnings("rawtypes") Class[] eventFilters() default {}; + /** + * Optional configuration of the maximal interval the SDK will wait for a reconciliation request + * to happen before one will be automatically triggered. + * + * @return the maximal interval configuration + */ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMaxInterval( - interval = 10, timeUnit = TimeUnit.HOURS); - + interval = 10); /** - * Optional list of classes providing {@link DependentResourceController} implementations - * encapsulating logic to handle the associated - * {@link io.javaoperatorsdk.operator.processing.Controller}'s reconciliation of dependent - * resources - * - * @return the list of {@link DependentResourceController} implementations + * Optional list of {@link Dependent} configurations which associate a resource type to a + * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} implementation + * + * @return the list of {@link Dependent} configurations */ Dependent[] dependents() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java deleted file mode 100644 index af57fe1b32..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContextInjector.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -public interface EventSourceContextInjector { - void injectInto(EventSourceContext context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index ea8b8bc06b..beac9ef4bb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -3,8 +3,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -public abstract class AbstractDependentResource - implements DependentResource { +public abstract class AbstractDependentResource + implements DependentResource { @Override public void reconcile(P primary, Context context) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java new file mode 100644 index 0000000000..21f0b4f55b --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +public @interface Dependent { + + Class type(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index b097554107..5208480166 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -3,24 +3,19 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -public interface DependentResource { +public interface DependentResource { Optional eventSource(EventSourceContext

context); - @SuppressWarnings("unchecked") - default Class resourceType() { - return (Class) Utils.getFirstTypeArgumentFromInterface(getClass()); - } - void reconcile(P primary, Context context); void delete(P primary, Context context); Optional getResource(P primaryResource); + default void configureWith(C config) {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java deleted file mode 100644 index b93d75950b..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredSupplier.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.javaoperatorsdk.operator.api.reconciler.Context; - -@FunctionalInterface -public interface DesiredSupplier { - - R getDesired(P primary, Context context); - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java new file mode 100644 index 0000000000..b8ef888ec3 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.client.KubernetesClient; + +public interface KubernetesClientAware { + void setKubernetesClient(KubernetesClient kubernetesClient); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java deleted file mode 100644 index d4d1e00dc2..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/StandaloneKubernetesDependentResource.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; - -// todo shorter name -public class StandaloneKubernetesDependentResource - extends KubernetesDependentResource { - - private final DesiredSupplier desiredSupplier; - private final Class resourceType; - private AssociatedSecondaryResourceIdentifier

associatedSecondaryResourceIdentifier = - ResourceID::fromResource; - private PrimaryResourcesRetriever primaryResourcesRetriever = Mappers.fromOwnerReference(); - - public StandaloneKubernetesDependentResource( - Class resourceType, DesiredSupplier desiredSupplier) { - this(null, resourceType, desiredSupplier); - } - - public StandaloneKubernetesDependentResource( - KubernetesClient client, Class resourceType, DesiredSupplier desiredSupplier) { - super(client); - this.desiredSupplier = desiredSupplier; - this.resourceType = resourceType; - } - - @Override - protected R desired(P primary, Context context) { - return desiredSupplier.getDesired(primary, context); - } - - public Class resourceType() { - return resourceType; - } - - public StandaloneKubernetesDependentResource setAssociatedSecondaryResourceIdentifier( - AssociatedSecondaryResourceIdentifier

associatedSecondaryResourceIdentifier) { - this.associatedSecondaryResourceIdentifier = associatedSecondaryResourceIdentifier; - return this; - } - - public StandaloneKubernetesDependentResource setPrimaryResourcesRetriever( - PrimaryResourcesRetriever primaryResourcesRetriever) { - this.primaryResourcesRetriever = primaryResourcesRetriever; - return this; - } - - @Override - protected AssociatedSecondaryResourceIdentifier

getDefaultAssociatedSecondaryResourceIdentifier() { - return this.associatedSecondaryResourceIdentifier; - } - - @Override - protected PrimaryResourcesRetriever getDefaultPrimaryResourcesRetriever() { - return this.primaryResourcesRetriever; - } -} 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 f3ad2dd8ec..89aabe0d9b 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 @@ -217,7 +217,6 @@ public void start() throws OperatorException { eventSourceManager.getControllerResourceEventSource().getResourceCache(), configurationService(), kubernetesClient); - dependents.injectInto(context); prepareEventSources(context).forEach(eventSourceManager::registerEventSource); eventSourceManager.start(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java deleted file mode 100644 index e49a55a73a..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceController.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; -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.dependent.DependentResource; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -@Ignore -public class DependentResourceController, D extends DependentResource> - implements DependentResource { - - private final D delegate; - private final C configuration; - - public DependentResourceController(D delegate, C configuration) { - this.delegate = delegate; - this.configuration = initConfiguration(delegate, configuration); - } - - protected C initConfiguration(D delegate, C configuration) { - // default implementation just returns the specified one - return configuration; - } - - @Override - public Class resourceType() { - return delegate.resourceType(); - } - - @Override - public void delete(P primary, Context context) { - delegate.delete(primary, context); - } - - @Override - public Optional getResource(P primaryResource) { - return delegate.getResource(primaryResource); - } - - - @Override - public Optional eventSource(EventSourceContext

context) { - return delegate.eventSource(context); - } - - - public C getConfiguration() { - return configuration; - } - - protected D delegate() { - return delegate; - } - - @Override - public void reconcile(P resource, Context context) { - delegate.reconcile(resource, context); - } - - -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index ac791526fc..6f3a98b4dd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -3,65 +3,63 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"rawtypes", "unchecked"}) @Ignore -public class DependentResourceManager

implements EventSourceInitializer

, - EventSourceContextInjector, Reconciler

{ - private final Reconciler

reconciler; - private final ControllerConfiguration

configuration; - private List dependents; +public class DependentResourceManager

+ implements EventSourceInitializer

, Reconciler

{ + + private static final Logger log = LoggerFactory.getLogger(DependentResourceManager.class); + private final Reconciler

reconciler; + private final ControllerConfiguration

controllerConfiguration; + private List dependents; public DependentResourceManager(Controller

controller) { this.reconciler = controller.getReconciler(); - this.configuration = controller.getConfiguration(); + this.controllerConfiguration = controller.getConfiguration(); } @Override public List prepareEventSources(EventSourceContext

context) { - final List configured = configuration.getDependentResources(); - dependents = new ArrayList<>(configured.size()); - - List sources = new ArrayList<>(configured.size() + 5); - configured.forEach(dependent -> { - final var dependentResourceController = from(dependent, context.getClient()); - dependents.add(dependentResourceController); - dependentResourceController.eventSource(context) - .ifPresent(es -> sources.add((EventSource) es)); - - }); - + final var dependentResources = controllerConfiguration.getDependentResources(); + final var sources = new ArrayList(dependentResources.size()); + dependents = + dependentResources.stream() + .map( + drc -> { + final var dependentResource = + from(drc, context.getClient()); + dependentResource + .eventSource(context) + .ifPresent(es -> sources.add((EventSource) es)); + return dependentResource; + }) + .collect(Collectors.toList()); return sources; } - @Override - public void injectInto(EventSourceContext context) { - if (reconciler instanceof EventSourceContextInjector) { - EventSourceContextInjector injector = (EventSourceContextInjector) reconciler; - injector.injectInto(context); - } - } - @Override public UpdateControl

reconcile(P resource, Context context) { initContextIfNeeded(resource, context); @@ -76,7 +74,6 @@ public DeleteControl cleanup(P resource, Context context) { return Reconciler.super.cleanup(resource, context); } - private void initContextIfNeeded(P resource, Context context) { if (reconciler instanceof ContextInitializer) { final var initializer = (ContextInitializer

) reconciler; @@ -84,29 +81,21 @@ private void initContextIfNeeded(P resource, Context context) { } } - private DependentResourceController from(DependentResourceConfiguration config, + private DependentResource from(DependentResourceSpec dependentResourceSpec, KubernetesClient client) { try { - final var dependentResource = - (DependentResource) config.getDependentResourceClass().getConstructor() - .newInstance(); - if (config instanceof KubernetesDependentResourceConfiguration) { - if (dependentResource instanceof KubernetesDependentResource) { - final var kubeDependentResource = (KubernetesDependentResource) dependentResource; - kubeDependentResource.setClient(client); - return new KubernetesDependentResourceController(kubeDependentResource, - (KubernetesDependentResourceConfiguration) config); - } else { - throw new IllegalArgumentException("A " - + KubernetesDependentResourceConfiguration.class.getCanonicalName() - + " must be associated to a " + KubernetesDependentResource.class.getCanonicalName()); - } - } else { - return new DependentResourceController(dependentResource, config); + DependentResource dependentResource = + (DependentResource) dependentResourceSpec.getDependentResourceClass() + .getConstructor().newInstance(); + if (dependentResource instanceof KubernetesClientAware) { + ((KubernetesClientAware) dependentResource).setKubernetesClient(client); } - } catch (NoSuchMethodException | InvocationTargetException | InstantiationException - | IllegalAccessException e) { - throw new IllegalArgumentException(e); + dependentResourceSpec.getDependentResourceConfiguration() + .ifPresent(dependentResource::configureWith); + return dependentResource; + } catch (InstantiationException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException e) { + throw new IllegalStateException(e); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java deleted file mode 100644 index 733460ef31..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/KubernetesDependentResourceController.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent; - -import java.util.Optional; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; -import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; - -@Ignore -public class KubernetesDependentResourceController - extends - DependentResourceController, KubernetesDependentResource> { - - public KubernetesDependentResourceController(KubernetesDependentResource delegate, - KubernetesDependentResourceConfiguration configuration) { - super(delegate, configuration); - } - - @SuppressWarnings("unchecked") - @Override - protected KubernetesDependentResourceConfiguration initConfiguration( - KubernetesDependentResource delegate, - KubernetesDependentResourceConfiguration configuration) { - // todo: check if we can validate that types actually match properly - final var associatedPrimaries = - (delegate instanceof PrimaryResourcesRetriever) - ? (PrimaryResourcesRetriever) delegate - : configuration.getPrimaryResourcesRetriever(); - final var associatedSecondary = - (delegate instanceof AssociatedSecondaryResourceIdentifier) - ? (AssociatedSecondaryResourceIdentifier

) delegate - : configuration.getAssociatedResourceIdentifier(); - - final var augmented = InformerConfiguration.from(configuration) - .withPrimaryResourcesRetriever(associatedPrimaries) - .withAssociatedSecondaryResourceIdentifier(associatedSecondary) - .build(); - return KubernetesDependentResourceConfiguration.from(augmented, configuration.isOwned(), - configuration.getDependentResourceClass()); - } - - @Override - public Optional eventSource(EventSourceContext

context) { - var informer = new InformerEventSource<>(getConfiguration(), context); - // todo have this implemented with nicer abstractions - delegate().setInformerEventSource(informer); - return super.eventSource(context); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java similarity index 73% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java index e7d2ad3158..c0922f569f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.config.dependent; +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -11,16 +11,14 @@ @Target({ElementType.TYPE}) public @interface KubernetesDependent { - boolean OWNED_DEFAULT = true; - boolean SKIP_UPDATE_DEFAULT = true; + boolean ADD_OWNER_REFERENCE_DEFAULT = true; - boolean owned() default OWNED_DEFAULT; - - boolean skipUpdateIfUnchanged() default SKIP_UPDATE_DEFAULT; + boolean addOwnerReference() default ADD_OWNER_REFERENCE_DEFAULT; /** * Specified which namespaces this Controller monitors for custom resources events. If no - * namespace is specified then the controller will monitor all namespaces by default. + * namespace is specified then the controller will monitor the namespaces configured for the + * controller. * * @return the list of namespaces this controller monitors */ @@ -34,4 +32,5 @@ * @return the label selector */ String labelSelector() default EMPTY_STRING; + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java similarity index 55% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index e8a4a6779e..a805c12b11 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -1,6 +1,7 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; import java.util.Optional; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,9 +9,13 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -19,25 +24,56 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; public abstract class KubernetesDependentResource - extends AbstractDependentResource { + extends AbstractDependentResource + implements KubernetesClientAware { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); protected KubernetesClient client; - private boolean explicitDelete = false; - private boolean owned = true; private InformerEventSource informerEventSource; + private boolean addOwnerReference; - public KubernetesDependentResource() { - this(null); + @Override + public void configureWith(KubernetesDependentResourceConfig config) { + configureWith(config.getConfigurationService(), config.labelSelector(), + Set.of(config.namespaces()), config.addOwnerReference()); } - public KubernetesDependentResource(KubernetesClient client) { - this.client = client; + @SuppressWarnings("unchecked") + private void configureWith(ConfigurationService service, String labelSelector, + Set namespaces, boolean addOwnerReference) { + final var primaryResourcesRetriever = + (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this + : Mappers.fromOwnerReference(); + final AssociatedSecondaryResourceIdentifier

secondaryResourceIdentifier = + (this instanceof AssociatedSecondaryResourceIdentifier) + ? (AssociatedSecondaryResourceIdentifier

) this + : ResourceID::fromResource; + InformerConfiguration ic = + InformerConfiguration.from(service, resourceType()) + .withLabelSelector(labelSelector) + .withNamespaces(namespaces) + .withPrimaryResourcesRetriever(primaryResourcesRetriever) + .withAssociatedSecondaryResourceIdentifier(secondaryResourceIdentifier) + .build(); + this.addOwnerReference = addOwnerReference; + informerEventSource = new InformerEventSource<>(ic, client); + } + + /** + * Use to share informers between event more resources. + * + * @param informerEventSource informer to use + * @param addOwnerReference to the created resource + */ + public void configureWith(InformerEventSource informerEventSource, + boolean addOwnerReference) { + this.informerEventSource = informerEventSource; + this.addOwnerReference = addOwnerReference; } protected void beforeCreateOrUpdate(R desired, P primary) { - if (owned) { + if (addOwnerReference) { desired.addOwnerReference(primary); } } @@ -69,42 +105,17 @@ protected R update(R actual, R target, P primary, Context context) { .replace(target); } - @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Optional eventSource(EventSourceContext

context) { - if (informerEventSource != null) { - return Optional.of(informerEventSource); + if (informerEventSource == null) { + configureWith(context.getConfigurationService(), null, null, + KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + log.warn("Using default configuration for " + resourceType().getSimpleName() + + " KubernetesDependentResource, call configureWith to provide configuration"); } - var informerConfig = initInformerConfiguration(context); - informerEventSource = new InformerEventSource(informerConfig, context); return Optional.of(informerEventSource); } - @SuppressWarnings("unchecked") - private InformerConfiguration initInformerConfiguration(EventSourceContext

context) { - PrimaryResourcesRetriever associatedPrimaries = - (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this - : getDefaultPrimaryResourcesRetriever(); - - AssociatedSecondaryResourceIdentifier

associatedSecondary = - (this instanceof AssociatedSecondaryResourceIdentifier) - ? (AssociatedSecondaryResourceIdentifier

) this - : getDefaultAssociatedSecondaryResourceIdentifier(); - - return InformerConfiguration.from(context, resourceType()) - .withPrimaryResourcesRetriever(associatedPrimaries) - .withAssociatedSecondaryResourceIdentifier(associatedSecondary) - .build(); - } - - protected AssociatedSecondaryResourceIdentifier

getDefaultAssociatedSecondaryResourceIdentifier() { - return ResourceID::fromResource; - } - - protected PrimaryResourcesRetriever getDefaultPrimaryResourcesRetriever() { - return Mappers.fromOwnerReference(); - } - public KubernetesDependentResource setInformerEventSource( InformerEventSource informerEventSource) { this.informerEventSource = informerEventSource; @@ -113,38 +124,24 @@ public KubernetesDependentResource setInformerEventSource( @Override public void delete(P primary, Context context) { - if (explicitDelete) { + if (addOwnerReference) { var resource = getResource(primary); resource.ifPresent(r -> client.resource(r).delete()); } } + @SuppressWarnings("unchecked") + protected Class resourceType() { + return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); + } + @Override public Optional getResource(P primaryResource) { return informerEventSource.getAssociated(primaryResource); } - public KubernetesDependentResource setClient(KubernetesClient client) { - this.client = client; - return this; - } - - - public KubernetesDependentResource setExplicitDelete(boolean explicitDelete) { - this.explicitDelete = explicitDelete; - return this; - } - - public boolean isExplicitDelete() { - return explicitDelete; - } - - public boolean isOwned() { - return owned; - } - - public KubernetesDependentResource setOwned(boolean owned) { - this.owned = owned; - return this; + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.client = kubernetesClient; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java new file mode 100644 index 0000000000..4464f3fd7d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.javaoperatorsdk.operator.api.config.ConfigurationService; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; +import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT; + +public class KubernetesDependentResourceConfig { + + private boolean addOwnerReference = ADD_OWNER_REFERENCE_DEFAULT; + private String[] namespaces = new String[0]; + private String labelSelector = EMPTY_STRING; + private ConfigurationService configurationService; + + public KubernetesDependentResourceConfig() {} + + public KubernetesDependentResourceConfig(boolean addOwnerReference, String[] namespaces, + String labelSelector, ConfigurationService configurationService) { + this.addOwnerReference = addOwnerReference; + this.namespaces = namespaces; + this.labelSelector = labelSelector; + this.configurationService = configurationService; + } + + public KubernetesDependentResourceConfig setAddOwnerReference( + boolean addOwnerReference) { + this.addOwnerReference = addOwnerReference; + return this; + } + + public KubernetesDependentResourceConfig setNamespaces(String[] namespaces) { + this.namespaces = namespaces; + return this; + } + + public KubernetesDependentResourceConfig setLabelSelector(String labelSelector) { + this.labelSelector = labelSelector; + return this; + } + + public KubernetesDependentResourceConfig setConfigurationService( + ConfigurationService configurationService) { + this.configurationService = configurationService; + return this; + } + + public boolean addOwnerReference() { + return addOwnerReference; + } + + public String[] namespaces() { + return namespaces; + } + + public String labelSelector() { + return labelSelector; + } + + public ConfigurationService getConfigurationService() { + return configurationService; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java deleted file mode 100644 index 95042d0c41..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSourceContextAware.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; - -public interface EventSourceContextAware

{ - void initWith(EventSourceContext

context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 3def733bd8..b29209f9e5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -5,12 +5,12 @@ import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; -import io.javaoperatorsdk.operator.processing.event.source.EventSourceContextAware; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; public class InformerEventSource @@ -23,17 +23,12 @@ public InformerEventSource(InformerConfiguration configuration, EventSourceContext

context) { super(context.getClient().resources(configuration.getResourceClass()), configuration); this.configuration = configuration; + } - // init mappers with context if needed - final var primaryResourcesRetriever = configuration.getPrimaryResourcesRetriever(); - if (primaryResourcesRetriever instanceof EventSourceContextAware) { - ((EventSourceContextAware) primaryResourcesRetriever).initWith(context); - } - - final var associatedResourceIdentifier = configuration.getAssociatedResourceIdentifier(); - if (associatedResourceIdentifier instanceof EventSourceContextAware) { - ((EventSourceContextAware) associatedResourceIdentifier).initWith(context); - } + public InformerEventSource(InformerConfiguration configuration, + KubernetesClient client) { + super(client.resources(configuration.getResourceClass()), configuration); + this.configuration = configuration; } @Override @@ -50,9 +45,8 @@ public void onUpdate(T oldObject, T newObject) { return; } - if (configuration.isSkipUpdateEventPropagationIfNoChange() && - oldObject.getMetadata().getResourceVersion() - .equals(newObject.getMetadata().getResourceVersion())) { + if (oldObject.getMetadata().getResourceVersion() + .equals(newObject.getMetadata().getResourceVersion())) { return; } propagateEvent(newObject); 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 f0a1a1185d..1dcfa2aa0e 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 @@ -34,7 +34,6 @@ public void start() { super.start(); } - @Override public void stop() { super.stop(); 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 new file mode 100644 index 0000000000..88dd8db469 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -0,0 +1,30 @@ +package io.javaoperatorsdk.operator.api.config; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.api.reconciler.Context; +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.*; + +class UtilsTest { + + @Test + void getsFirstTypeArgumentFromExtendedClass() { + Class res = + Utils.getFirstTypeArgumentFromExtendedClass(TestKubernetesDependentResource.class); + assertThat(res).isEqualTo(Deployment.class); + } + + public static class TestKubernetesDependentResource + extends KubernetesDependentResource { + + @Override + protected Deployment desired(TestCustomResource primary, Context context) { + return null; + } + } +} diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java index 76d848c896..c445acf67c 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/OperatorExtension.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -15,6 +16,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.retry.Retry; @@ -37,7 +39,11 @@ private OperatorExtension( boolean preserveNamespaceOnError, boolean waitForNamespaceDeletion, boolean oneNamespacePerClass) { - super(configurationService, infrastructure, infrastructureTimeout, oneNamespacePerClass, + super( + configurationService, + infrastructure, + infrastructureTimeout, + oneNamespacePerClass, preserveNamespaceOnError, waitForNamespaceDeletion); this.reconcilers = reconcilers; @@ -86,6 +92,9 @@ protected void before(ExtensionContext context) { if (ref.retry != null) { oconfig.withRetry(ref.retry); } + if (ref.controllerConfigurationOverrider != null) { + ref.controllerConfigurationOverrider.accept(oconfig); + } final var kubernetesClient = getKubernetesClient(); try (InputStream is = getClass().getResourceAsStream(path)) { @@ -127,6 +136,19 @@ protected Builder() { this.reconcilers = new ArrayList<>(); } + public Builder withReconciler( + Reconciler value, Consumer configurationOverrider) { + return withReconciler(value, null, configurationOverrider); + } + + public Builder withReconciler( + Reconciler value, + Retry retry, + Consumer configurationOverrider) { + reconcilers.add(new ReconcilerSpec(value, retry, configurationOverrider)); + return this; + } + @SuppressWarnings("rawtypes") public Builder withReconciler(Reconciler value) { reconcilers.add(new ReconcilerSpec(value, null)); @@ -165,10 +187,19 @@ public OperatorExtension build() { private static class ReconcilerSpec { final Reconciler reconciler; final Retry retry; + final Consumer controllerConfigurationOverrider; public ReconcilerSpec(Reconciler reconciler, Retry retry) { + this(reconciler, retry, null); + } + + public ReconcilerSpec( + Reconciler reconciler, + Retry retry, + Consumer controllerConfigurationOverrider) { this.reconciler = reconciler; this.retry = retry; + this.controllerConfigurationOverrider = controllerConfigurationOverrider; } } } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java similarity index 50% rename from operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java index 19e31e932b..dd1e005fc1 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationConfiguration.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfiguration.java @@ -1,37 +1,34 @@ package io.javaoperatorsdk.operator.config.runtime; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.function.Function; +import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependentResourceConfiguration; -import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; 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.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @SuppressWarnings("rawtypes") -public class AnnotationConfiguration +public class AnnotationControllerConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; - private List dependentConfigurations; - public AnnotationConfiguration(Reconciler reconciler) { + public AnnotationControllerConfiguration(Reconciler reconciler) { this.reconciler = reconciler; this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); } @@ -50,16 +47,17 @@ public String getFinalizer() { 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"); + 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); + return valueOrDefault( + annotation, ControllerConfiguration::generationAwareEventProcessing, true); } @Override @@ -99,8 +97,7 @@ public ResourceEventFilter getEventFilter() { Class>[] filterTypes = (Class>[]) valueOrDefault(annotation, - ControllerConfiguration::eventFilters, - new Object[] {}); + ControllerConfiguration::eventFilters, new Object[] {}); if (filterTypes.length > 0) { for (var filterType : filterTypes) { try { @@ -116,9 +113,7 @@ public ResourceEventFilter getEventFilter() { } } } - return answer != null - ? answer - : ResourceEventFilters.passthrough(); + return answer != null ? answer : ResourceEventFilters.passthrough(); } @Override @@ -127,8 +122,10 @@ public Optional reconciliationMaxInterval() { if (annotation.reconciliationMaxInterval().interval() <= 0) { return Optional.empty(); } - return Optional.of(Duration.of(annotation.reconciliationMaxInterval().interval(), - annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); + return Optional.of( + Duration.of( + annotation.reconciliationMaxInterval().interval(), + annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); } else { return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.reconciliationMaxInterval(); } @@ -146,59 +143,41 @@ public static T valueOrDefault( } @Override - public List getDependentResources() { - if (dependentConfigurations == null) { - final var dependents = valueOrDefault(annotation, ControllerConfiguration::dependents, - new Dependent[] {}); - if (dependents.length > 0) { - dependentConfigurations = new ArrayList<>(dependents.length); - for (Dependent dependent : dependents) { - final Class dependentType = dependent.type(); - final var resourceType = dependent.resourceType(); - - if (HasMetadata.class.isAssignableFrom(resourceType)) { - final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); - final var namespaces = - valueOrDefault(kubeDependent, KubernetesDependent::namespaces, - this.getNamespaces().toArray(new String[0])); - final var labelSelector = - valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); - final var owned = valueOrDefault(kubeDependent, KubernetesDependent::owned, - KubernetesDependent.OWNED_DEFAULT); - final var skipIfUnchanged = - valueOrDefault(kubeDependent, KubernetesDependent::skipUpdateIfUnchanged, - KubernetesDependent.SKIP_UPDATE_DEFAULT); - final var configuration = InformerConfiguration.from(service, resourceType) - .withLabelSelector(labelSelector) - .skippingEventPropagationIfUnchanged(skipIfUnchanged) - .withNamespaces(namespaces) - .build(); - - dependentConfigurations.add( - KubernetesDependentResourceConfiguration.from(configuration, owned, dependentType)); - } else { - dependentConfigurations.add(new DependentResourceConfiguration() { - @Override - public Class getDependentResourceClass() { - return dependentType; - } - - @Override - public Class getResourceClass() { - return resourceType; - } - }); - } - } + public List getDependentResources() { + final var dependents = + valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); + if (dependents.length == 0) { + return Collections.emptyList(); + } + + List resourceSpecs = new ArrayList<>(dependents.length); + for (Dependent dependent : dependents) { + + final Class dependentType = dependent.type(); + if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { + final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); + final var namespaces = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::namespaces, + this.getNamespaces().toArray(new String[0])); + final var labelSelector = + Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); + final var addOwnerReference = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::addOwnerReference, + KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + KubernetesDependentResourceConfig config = + new KubernetesDependentResourceConfig( + addOwnerReference, namespaces, labelSelector, getConfigurationService()); + resourceSpecs.add(new DependentResourceSpec(dependentType, config)); } else { - dependentConfigurations = Collections.emptyList(); + resourceSpecs.add(new DependentResourceSpec(dependentType)); } } - return dependentConfigurations; - } - - private static T valueOrDefault(C annotation, Function mapper, T defaultValue) { - return annotation == null ? defaultValue : mapper.apply(annotation); + return Arrays.stream(dependents) + .map(d -> new DependentResourceSpec(d.type())) + .collect(Collectors.toList()); } } - 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 44fc674028..21af48cfd1 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 @@ -30,7 +30,7 @@ ControllerConfiguration getConfigurationFor( if (config == null) { if (createIfNeeded) { // create the configuration on demand and register it - config = new AnnotationConfiguration<>(reconciler); + config = new AnnotationControllerConfiguration<>(reconciler); register(config); getLogger().info( "Created configuration for reconciler {} with name {}", diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 695cc30a31..b168d8fb47 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -9,8 +9,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.api.reconciler.dependent.StandaloneKubernetesDependentResource; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -23,24 +23,10 @@ public class StandaloneDependentTestReconciler private KubernetesClient kubernetesClient; - StandaloneKubernetesDependentResource configMapDependent; + KubernetesDependentResource configMapDependent; public StandaloneDependentTestReconciler() { - configMapDependent = - new StandaloneKubernetesDependentResource<>(Deployment.class, (primary, context) -> { - Deployment deployment = loadYaml(Deployment.class, "nginx-deployment.yaml"); - deployment.getMetadata().setName(primary.getMetadata().getName()); - deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); - return deployment; - }) { - @Override - protected boolean match(Deployment actual, Deployment target, Context context) { - return Objects.equals(actual.getSpec().getReplicas(), target.getSpec().getReplicas()) && - actual.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() - .equals( - target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); - } - }; + configMapDependent = new DeploymentDependentResource(); } @Override @@ -59,7 +45,7 @@ public UpdateControl reconcile( @Override public void setKubernetesClient(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; - configMapDependent.setClient(kubernetesClient); + configMapDependent.setKubernetesClient(kubernetesClient); } @Override @@ -74,4 +60,26 @@ private T loadYaml(Class clazz, String yaml) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); } } + + private class DeploymentDependentResource extends + KubernetesDependentResource { + + @Override + protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { + Deployment deployment = StandaloneDependentTestReconciler.this.loadYaml(Deployment.class, + "nginx-deployment.yaml"); + deployment.getMetadata().setName(primary.getMetadata().getName()); + deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + return deployment; + } + + @Override + protected boolean match(Deployment actual, Deployment target, Context context) { + return Objects.equals(actual.getSpec().getReplicas(), target.getSpec().getReplicas()) && + actual.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals( + target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } + + } } 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 d9012637cf..4fddf8cadc 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 @@ -15,6 +15,9 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.config.runtime.AnnotationControllerConfiguration; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; @@ -32,7 +35,17 @@ public static void main(String[] args) throws IOException { new ConfigurationServiceOverrider(DefaultConfigurationService.instance()) .withMetrics(new MicrometerMetrics(new LoggingMeterRegistry())) .build()); - operator.register(new MySQLSchemaReconciler(MySQLDbConfig.loadFromEnvironmentVars())); + + MySQLSchemaReconciler schemaReconciler = new MySQLSchemaReconciler(); + ControllerConfigurationOverrider configOverrider = + ControllerConfigurationOverrider + .override(new AnnotationControllerConfiguration(schemaReconciler)); + + configOverrider.replaceDependentResourceConfig( + new DependentResourceSpec(SchemaDependentResource.class, + new ResourcePollerConfig(500, MySQLDbConfig.loadFromEnvironmentVars()))); + + operator.register(schemaReconciler, configOverrider.build()); operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 63c37ac3d7..8965ae2a89 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -6,17 +6,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.Secret; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContextInjector; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.sample.schema.Schema; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -25,12 +22,12 @@ // todo handle this, should work with finalizer @ControllerConfiguration(finalizerName = NO_FINALIZER, dependents = { - @Dependent(resourceType = Secret.class, type = SecretDependentResource.class), - @Dependent(resourceType = Schema.class, type = SchemaDependentResource.class) + @Dependent(type = SecretDependentResource.class), + @Dependent(type = SchemaDependentResource.class) }) public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler, - ContextInitializer, EventSourceContextInjector { + ContextInitializer { static final String SECRET_FORMAT = "%s-secret"; static final String USERNAME_FORMAT = "%s-user"; @@ -38,21 +35,10 @@ public class MySQLSchemaReconciler static final String MYSQL_SECRET_NAME = "mysql.secret.name"; static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; - static final String MYSQL_DB_CONFIG = "mysql.db.config"; static final String BUILT_SCHEMA = "built schema"; static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class); - private final MySQLDbConfig mysqlDbConfig; - - public MySQLSchemaReconciler(MySQLDbConfig mysqlDbConfig) { - this.mysqlDbConfig = mysqlDbConfig; - } - - @SuppressWarnings("rawtypes") - @Override - public void injectInto(EventSourceContext context) { - context.put(MYSQL_DB_CONFIG, mysqlDbConfig); - } + public MySQLSchemaReconciler() {} @Override public void initContext(MySQLSchema primary, Context context) { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java new file mode 100644 index 0000000000..924c3978d1 --- /dev/null +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample; + +public class ResourcePollerConfig { + + private final int pollPeriod; + private final MySQLDbConfig mySQLDbConfig; + + + public ResourcePollerConfig(int pollPeriod, MySQLDbConfig mySQLDbConfig) { + this.pollPeriod = pollPeriod; + this.mySQLDbConfig = mySQLDbConfig; + } + + public int getPollPeriod() { + return pollPeriod; + } + + public MySQLDbConfig getMySQLDbConfig() { + return mySQLDbConfig; + } +} diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 957f0b34ff..62ce1356a2 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -15,16 +15,22 @@ import static java.lang.String.format; -public class SchemaDependentResource extends AbstractDependentResource { +public class SchemaDependentResource extends + AbstractDependentResource { - private static final int POLL_PERIOD = 500; private MySQLDbConfig dbConfig; + private int pollPeriod; + + @Override + public void configureWith(ResourcePollerConfig config) { + this.dbConfig = config.getMySQLDbConfig(); + this.pollPeriod = config.getPollPeriod(); + } @Override public Optional eventSource(EventSourceContext context) { - dbConfig = context.getMandatory(MySQLSchemaReconciler.MYSQL_DB_CONFIG, MySQLDbConfig.class); return Optional.of(new PerResourcePollingEventSource<>( - new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), POLL_PERIOD, + new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), pollPeriod, Schema.class)); } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java index 4f6bd1759c..884cc6d2c9 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -18,8 +18,16 @@ private static String encode(String value) { return Base64.getEncoder().encodeToString(value.getBytes()); } + // An alternative would be to override reconcile() method and exclude the update part. + @Override + protected Secret update(Secret actual, Secret target, MySQLSchema primary, Context context) { + throw new IllegalStateException( + "Secret should not be updated. Secret: " + target + " for custom resource: " + + primary); + } + @Override - public Secret desired(MySQLSchema schema, Context context) { + protected Secret desired(MySQLSchema schema, Context context) { return new SecretBuilder() .withNewMetadata() .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) @@ -32,14 +40,6 @@ public Secret desired(MySQLSchema schema, Context context) { .build(); } - // An alternative would be to override reconcile() method and exclude the update part. - @Override - protected Secret update(Secret actual, Secret target, MySQLSchema primary, Context context) { - throw new IllegalStateException( - "Secret should not be updated. Secret: " + target + " for custom resource: " - + primary); - } - @Override protected boolean match(Secret actual, Secret target, Context context) { return ResourceID.fromResource(actual).equals(ResourceID.fromResource(target)); 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 0c1be5efc0..f433671c08 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 @@ -17,6 +17,7 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.LocalPortForward; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; @@ -31,25 +32,20 @@ class MySQLSchemaOperatorE2E { - final static Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); + static final Logger log = LoggerFactory.getLogger(MySQLSchemaOperatorE2E.class); - final static KubernetesClient client = new DefaultKubernetesClient(); + static final KubernetesClient client = new DefaultKubernetesClient(); - final static String MY_SQL_NS = "mysql"; + static final String MY_SQL_NS = "mysql"; private static List infrastructure = new ArrayList<>(); + static { - infrastructure - .add(new NamespaceBuilder() - .withNewMetadata() - .withName(MY_SQL_NS) - .endMetadata() - .build()); + infrastructure.add( + new NamespaceBuilder().withNewMetadata().withName(MY_SQL_NS).endMetadata().build()); try { - infrastructure.addAll( - client.load(new FileInputStream("k8s/mysql-deployment.yaml")).get()); - infrastructure.addAll( - client.load(new FileInputStream("k8s/mysql-service.yaml")).get()); + infrastructure.addAll(client.load(new FileInputStream("k8s/mysql-deployment.yaml")).get()); + infrastructure.addAll(client.load(new FileInputStream("k8s/mysql-service.yaml")).get()); } catch (FileNotFoundException e) { e.printStackTrace(); } @@ -63,20 +59,26 @@ boolean isLocal() { } @RegisterExtension - AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler( - new MySQLSchemaReconciler(new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))) - .withInfrastructure(infrastructure) - .build() - : E2EOperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withOperatorDeployment( - client.load(new FileInputStream("k8s/operator.yaml")).get()) - .withInfrastructure(infrastructure) - .build(); - - + AbstractOperatorExtension operator = + isLocal() + ? OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler( + new MySQLSchemaReconciler(), + c -> { + c.replaceDependentResourceConfig( + new DependentResourceSpec( + SchemaDependentResource.class, + new ResourcePollerConfig( + 700, new MySQLDbConfig("127.0.0.1", "3306", "root", "password")))); + }) + .withInfrastructure(infrastructure) + .build() + : E2EOperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get()) + .withInfrastructure(infrastructure) + .build(); public MySQLSchemaOperatorE2E() throws FileNotFoundException {} @@ -85,28 +87,23 @@ public void test() throws IOException { // Opening a port-forward if running locally LocalPortForward portForward = null; if (isLocal()) { - String podName = client - .pods() - .inNamespace(MY_SQL_NS) - .withLabel("app", "mysql") - .list() - .getItems() - .get(0) - .getMetadata() - .getName(); - - portForward = client - .pods() - .inNamespace(MY_SQL_NS) - .withName(podName) - .portForward(3306, 3306); + String podName = + client + .pods() + .inNamespace(MY_SQL_NS) + .withLabel("app", "mysql") + .list() + .getItems() + .get(0) + .getMetadata() + .getName(); + + portForward = client.pods().inNamespace(MY_SQL_NS).withName(podName).portForward(3306, 3306); } MySQLSchema testSchema = new MySQLSchema(); - testSchema.setMetadata(new ObjectMetaBuilder() - .withName("mydb1") - .withNamespace(operator.getNamespace()) - .build()); + testSchema.setMetadata( + new ObjectMetaBuilder().withName("mydb1").withNamespace(operator.getNamespace()).build()); testSchema.setSpec(new SchemaSpec()); testSchema.getSpec().setEncoding("utf8"); @@ -114,19 +111,25 @@ public void test() throws IOException { client.resource(testSchema).createOrReplace(); log.info("Waiting 2 minutes for expected resources to be created and updated"); - await().atMost(2, MINUTES).ignoreExceptions().untilAsserted(() -> { - MySQLSchema updatedSchema = - client.resources(MySQLSchema.class).inNamespace(operator.getNamespace()) - .withName(testSchema.getMetadata().getName()).get(); - assertThat(updatedSchema.getStatus(), is(notNullValue())); - assertThat(updatedSchema.getStatus().getStatus(), equalTo("CREATED")); - assertThat(updatedSchema.getStatus().getSecretName(), is(notNullValue())); - assertThat(updatedSchema.getStatus().getUserName(), is(notNullValue())); - }); + await() + .atMost(2, MINUTES) + .ignoreExceptions() + .untilAsserted( + () -> { + MySQLSchema updatedSchema = + client + .resources(MySQLSchema.class) + .inNamespace(operator.getNamespace()) + .withName(testSchema.getMetadata().getName()) + .get(); + assertThat(updatedSchema.getStatus(), is(notNullValue())); + assertThat(updatedSchema.getStatus().getStatus(), equalTo("CREATED")); + assertThat(updatedSchema.getStatus().getSecretName(), is(notNullValue())); + assertThat(updatedSchema.getStatus().getUserName(), is(notNullValue())); + }); if (portForward != null) { portForward.close(); } } - } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index b681ccf6b6..a0c22ce9b1 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -3,18 +3,19 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; -import io.javaoperatorsdk.operator.api.config.dependent.KubernetesDependent; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") -public class DeploymentDependentResource - extends KubernetesDependentResource { +public class DeploymentDependentResource extends KubernetesDependentResource { - public DeploymentDependentResource() {} + private static String tomcatImage(Tomcat tomcat) { + return "tomcat:" + tomcat.getSpec().getVersion(); + } @Override - public Deployment desired(Tomcat tomcat, Context context) { + protected Deployment desired(Tomcat tomcat, Context context) { Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); @@ -31,7 +32,8 @@ public Deployment desired(Tomcat tomcat, Context context) { .withReplicas(tomcat.getSpec().getReplicas()) // set tomcat version .editTemplate() - // make sure label selector matches label (which has to be matched by service selector too) + // make sure label selector matches label (which has to be matched by service selector + // too) .editMetadata().addToLabels("app", tomcatName).endMetadata() .editSpec() .editFirstContainer().withImage(tomcatImage(tomcat)).endContainer() @@ -42,13 +44,10 @@ public Deployment desired(Tomcat tomcat, Context context) { return deployment; } - private String tomcatImage(Tomcat tomcat) { - return "tomcat:" + tomcat.getSpec().getVersion(); - } - @Override public boolean match(Deployment fetched, Deployment target, Context context) { return fetched.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() .equals(target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); } + } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 16a6aaff2e..df690f6abd 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -4,14 +4,12 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; public class ServiceDependentResource extends KubernetesDependentResource { - public ServiceDependentResource() {} - @Override - public Service desired(Tomcat tomcat, Context context) { + protected Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); return new ServiceBuilder(TomcatReconciler.loadYaml(Service.class, "service.yaml")) .editMetadata() @@ -23,4 +21,5 @@ public Service desired(Tomcat tomcat, Context context) { .endSpec() .build(); } + } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index d2d2a6c96c..c84ff61eb1 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -7,15 +7,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; import io.fabric8.kubernetes.client.utils.Serialization; -import io.javaoperatorsdk.operator.api.config.dependent.Dependent; 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 static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -26,8 +25,8 @@ @ControllerConfiguration( finalizerName = NO_FINALIZER, dependents = { - @Dependent(resourceType = Deployment.class, type = DeploymentDependentResource.class), - @Dependent(resourceType = Service.class, type = ServiceDependentResource.class) + @Dependent(type = DeploymentDependentResource.class), + @Dependent(type = ServiceDependentResource.class) }) public class TomcatReconciler implements Reconciler { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 28d6ed3037..cc9bce6c6d 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -15,8 +15,9 @@ import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.StandaloneKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -30,9 +31,9 @@ public class WebPageReconciler private final KubernetesClient kubernetesClient; - private StandaloneKubernetesDependentResource configMapDR; - private StandaloneKubernetesDependentResource deploymentDR; - private StandaloneKubernetesDependentResource serviceDR; + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; @@ -42,9 +43,9 @@ public WebPageReconciler(KubernetesClient kubernetesClient) { @Override public List prepareEventSources(EventSourceContext context) { List eventSources = new ArrayList<>(3); - configMapDR.eventSource(context).ifPresent(es -> eventSources.add(es)); - deploymentDR.eventSource(context).ifPresent(es -> eventSources.add(es)); - serviceDR.eventSource(context).ifPresent(es -> eventSources.add(es)); + configMapDR.eventSource(context).ifPresent(eventSources::add); + deploymentDR.eventSource(context).ifPresent(eventSources::add); + serviceDR.eventSource(context).ifPresent(eventSources::add); return eventSources; } @@ -83,97 +84,71 @@ public Optional updateErrorStatus( } private void createDependentResources(KubernetesClient client) { - this.configMapDR = - new StandaloneKubernetesDependentResource<>( - client, - ConfigMap.class, - (WebPage webPage, Context context) -> { - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - return new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(configMapName(webPage)) - .withNamespace(webPage.getMetadata().getNamespace()) - .build()) - .withData(data) - .build(); - }) { - @Override - protected boolean match(ConfigMap actual, ConfigMap target, Context context) { - return StringUtils.equals( - actual.getData().get("index.html"), target.getData().get("index.html")); - } + this.configMapDR = new ConfigMapDependentResource(); + + this.deploymentDR = + new KubernetesDependentResource<>() { @Override - protected ConfigMap update( - ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - var cm = super.update(actual, target, primary, context); - var ns = actual.getMetadata().getNamespace(); - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(primary)) - .delete(); - return cm; + protected Deployment desired(WebPage webPage, Context context) { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; } - }; - configMapDR.setAssociatedSecondaryResourceIdentifier( - primary -> new ResourceID(configMapName(primary), primary.getMetadata().getNamespace())); - this.deploymentDR = - new StandaloneKubernetesDependentResource<>( - client, - Deployment.class, - (webPage, context) -> { - var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); - return deployment; - }) { @Override protected boolean match(Deployment actual, Deployment target, Context context) { // todo comparator return true; } + + @Override + protected Class resourceType() { + return Deployment.class; + } }; this.serviceDR = - new StandaloneKubernetesDependentResource<>( - client, - Service.class, - (webPage, context) -> { - Service service = loadYaml(Service.class, "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - Map labels = new HashMap<>(); - labels.put("app", deploymentName(webPage)); - service.getSpec().setSelector(labels); - return service; - }) { + new KubernetesDependentResource<>() { + + @Override + protected Service desired(WebPage webPage, Context context) { + Service service = loadYaml(Service.class, "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + } protected boolean match(Service actual, Service target, Context context) { // todo comparator return true; } + + @Override + protected Class resourceType() { + return Service.class; + } }; } @@ -196,4 +171,48 @@ private T loadYaml(Class clazz, String yaml) { throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); } } + + private class ConfigMapDependentResource extends KubernetesDependentResource + implements + AssociatedSecondaryResourceIdentifier { + + @Override + protected ConfigMap desired(WebPage webPage, Context context) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(WebPageReconciler.configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + } + + @Override + protected boolean match(ConfigMap actual, ConfigMap target, Context context) { + return StringUtils.equals( + actual.getData().get("index.html"), target.getData().get("index.html")); + } + + @Override + protected ConfigMap update( + ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + var cm = super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + return cm; + } + + @Override + public ResourceID associatedSecondaryID(WebPage primary) { + return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); + } + } } From 3302b99f7ecce4bc162503953294ddc769794c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Feb 2022 14:16:30 +0100 Subject: [PATCH 0304/1608] fix: not delete if add owner reference (#946) --- .../dependent/kubernetes/KubernetesDependentResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a805c12b11..7a1a2e7afb 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 @@ -124,7 +124,7 @@ public KubernetesDependentResource setInformerEventSource( @Override public void delete(P primary, Context context) { - if (addOwnerReference) { + if (!addOwnerReference) { var resource = getResource(primary); resource.ifPresent(r -> client.resource(r).delete()); } From f205fd47851158620528460f8cac732c5331d644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Feb 2022 17:06:46 +0100 Subject: [PATCH 0305/1608] Nicer config override (#944) --- .../io/javaoperatorsdk/operator/Operator.java | 19 ++++++++++++++++++- .../ControllerConfigurationOverrider.java | 18 ++++++++---------- .../dependent/DependentResourceManager.java | 4 ++-- .../operator/OperatorTest.java | 2 +- .../operator/sample/MySQLSchemaOperator.java | 13 +++---------- .../sample/SchemaDependentResource.java | 5 ++++- .../sample/MySQLSchemaOperatorE2E.java | 8 +++----- 7 files changed, 39 insertions(+), 30 deletions(-) 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 5f71ec2747..2b15ab5cda 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 @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +15,7 @@ import io.fabric8.kubernetes.client.Version; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.Controller; @@ -118,7 +120,7 @@ public void register(Reconciler reconciler) * * @param reconciler part of the reconciler to register * @param configuration the configuration with which we want to register the reconciler - * @param the {@code CustomResource} type associated with the reconciler + * @param the {@code HasMetadata} type associated with the reconciler * @throws OperatorException if a problem occurred during the registration process */ public void register(Reconciler reconciler, @@ -147,6 +149,21 @@ public void register(Reconciler reconciler, watchedNS); } + /** + * Method to register operator and facilitate configuration override. + * + * @param reconciler part of the reconciler to register + * @param configOverrider consumer to use to change config values + * @param the {@code HasMetadata} type associated with the reconciler + */ + public void register(Reconciler reconciler, + Consumer configOverrider) { + final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); + var configToOverride = ControllerConfigurationOverrider.override(controllerConfiguration); + configOverrider.accept(configToOverride); + register(reconciler, configToOverride.build()); + } + static class ControllerManager implements LifecycleAware { private final Map controllers = new HashMap<>(); private boolean started = false; 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 141b58ded0..7fb919abb1 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 @@ -88,21 +88,19 @@ public ControllerConfigurationOverrider withReconciliationMaxInterval( return this; } - /** - * If a {@link DependentResourceSpec} already exists with the same dependentResourceClass it will - * be replaced. Otherwise, an exception is thrown; - * - * @param dependentResourceSpec to add or replace - */ - public void replaceDependentResourceConfig(DependentResourceSpec dependentResourceSpec) { + public void replaceDependentResourceConfig( + Class> dependentResourceClass, + Object dependentResourceConfig) { + var currentConfig = - findConfigForDependentResourceClass(dependentResourceSpec.getDependentResourceClass()); + findConfigForDependentResourceClass(dependentResourceClass); if (currentConfig.isEmpty()) { throw new IllegalStateException("Cannot find DependentResource config for class: " - + dependentResourceSpec.getDependentResourceClass()); + + dependentResourceClass); } dependentResourceSpecs.remove(currentConfig.get()); - dependentResourceSpecs.add(dependentResourceSpec); + dependentResourceSpecs + .add(new DependentResourceSpec(dependentResourceClass, dependentResourceConfig)); } public void addNewDependentResourceConfig(DependentResourceSpec dependentResourceSpec) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 6f3a98b4dd..102ffef088 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -50,7 +50,7 @@ public List prepareEventSources(EventSourceContext

context) { .map( drc -> { final var dependentResource = - from(drc, context.getClient()); + createAndConfigureFrom(drc, context.getClient()); dependentResource .eventSource(context) .ifPresent(es -> sources.add((EventSource) es)); @@ -81,7 +81,7 @@ private void initContextIfNeeded(P resource, Context context) { } } - private DependentResource from(DependentResourceSpec dependentResourceSpec, + private DependentResource createAndConfigureFrom(DependentResourceSpec dependentResourceSpec, KubernetesClient client) { try { DependentResource dependentResource = diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index f532756f58..99bd7e64b9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -47,7 +47,7 @@ public void shouldRegisterReconcilerToController() { @Test @DisplayName("should throw `OperationException` when Configuration is null") public void shouldThrowOperatorExceptionWhenConfigurationIsNull() { - Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler, null)); + Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler)); } private static class FooCustomResource extends CustomResource { 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 4fddf8cadc..5a39408ec8 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 @@ -15,9 +15,6 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; -import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.config.runtime.AnnotationControllerConfiguration; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; @@ -37,15 +34,11 @@ public static void main(String[] args) throws IOException { .build()); MySQLSchemaReconciler schemaReconciler = new MySQLSchemaReconciler(); - ControllerConfigurationOverrider configOverrider = - ControllerConfigurationOverrider - .override(new AnnotationControllerConfiguration(schemaReconciler)); - configOverrider.replaceDependentResourceConfig( - new DependentResourceSpec(SchemaDependentResource.class, + operator.register(schemaReconciler, + configOverrider -> configOverrider.replaceDependentResourceConfig( + SchemaDependentResource.class, new ResourcePollerConfig(500, MySQLDbConfig.loadFromEnvironmentVars()))); - - operator.register(schemaReconciler, configOverrider.build()); operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 62ce1356a2..338a420b65 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -19,7 +19,7 @@ public class SchemaDependentResource extends AbstractDependentResource { private MySQLDbConfig dbConfig; - private int pollPeriod; + private int pollPeriod = 500; @Override public void configureWith(ResourcePollerConfig config) { @@ -29,6 +29,9 @@ public void configureWith(ResourcePollerConfig config) { @Override public Optional eventSource(EventSourceContext context) { + if (dbConfig == null) { + dbConfig = MySQLDbConfig.loadFromEnvironmentVars(); + } return Optional.of(new PerResourcePollingEventSource<>( new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), pollPeriod, Schema.class)); 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 f433671c08..2ebece8aa2 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 @@ -17,7 +17,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.LocalPortForward; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; @@ -67,10 +66,9 @@ boolean isLocal() { new MySQLSchemaReconciler(), c -> { c.replaceDependentResourceConfig( - new DependentResourceSpec( - SchemaDependentResource.class, - new ResourcePollerConfig( - 700, new MySQLDbConfig("127.0.0.1", "3306", "root", "password")))); + SchemaDependentResource.class, + new ResourcePollerConfig( + 700, new MySQLDbConfig("127.0.0.1", "3306", "root", "password"))); }) .withInfrastructure(infrastructure) .build() From dbf6c2e001ff5bba701f231babc9affc8c7433a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Feb 2022 17:22:22 +0100 Subject: [PATCH 0306/1608] feat: generic matcher for Kubernetes dependent resource (#945) --- .../operator/ReconcilerUtils.java | 13 ++++- .../api/config/ConfigurationService.java | 8 ++- .../kubernetes/DesiredValueMatcher.java | 43 ++++++++++++++++ .../KubernetesDependentResource.java | 33 ++++++++---- .../kubernetes/ResourceComparators.java | 20 ++++++++ .../dependent/kubernetes/ResourceMatcher.java | 9 ++++ .../kubernetes/DesiredValueMatcherTest.java | 50 +++++++++++++++++++ .../kubernetes/nginx-deployment.yaml | 21 ++++++++ .../StandaloneDependentTestReconciler.java | 10 ---- .../sample/DeploymentDependentResource.java | 7 --- .../operator/sample/WebPageReconciler.java | 13 +---- 11 files changed, 186 insertions(+), 41 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceComparators.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java create mode 100644 operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 6f4e19692b..491910d13e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -1,11 +1,14 @@ package io.javaoperatorsdk.operator; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Locale; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -111,7 +114,15 @@ public static Object getSpec(HasMetadata resource) { Method getSpecMethod = resource.getClass().getMethod("getSpec"); return getSpecMethod.invoke(resource); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw new IllegalStateException(e); + throw new IllegalStateException("No spec found on resource", e); + } + } + + public static T loadYaml(Class clazz, Class loader, String yaml) { + try (InputStream is = loader.getResourceAsStream(yaml)) { + return Serialization.unmarshal(is, clazz); + } catch (IOException ex) { + throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); } } 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 2e4679efd3..6e5f44ebea 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 @@ -16,9 +16,9 @@ /** An interface from which to retrieve configuration information. */ public interface ConfigurationService { - Cloner DEFAULT_CLONER = new Cloner() { - private final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + Cloner DEFAULT_CLONER = new Cloner() { @Override public HasMetadata clone(HasMetadata object) { try { @@ -122,4 +122,8 @@ default ExecutorService getExecutorService() { default boolean closeClientOnStop() { return true; } + + default ObjectMapper getObjectMapper() { + return OBJECT_MAPPER; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java new file mode 100644 index 0000000000..b8c9b1c7a5 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java @@ -0,0 +1,43 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.zjsonpatch.JsonDiff; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class DesiredValueMatcher implements ResourceMatcher { + + private final ObjectMapper objectMapper; + + public DesiredValueMatcher(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context) { + if (actualResource instanceof Secret) { + return ResourceComparators.compareSecretData((Secret) desiredResource, + (Secret) actualResource); + } + if (actualResource instanceof ConfigMap) { + return ResourceComparators.compareConfigMapData((ConfigMap) desiredResource, + (ConfigMap) actualResource); + } + // reflection will be replaced by this: + // https://github.com/fabric8io/kubernetes-client/issues/3816 + var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desiredResource)); + var actualSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(actualResource)); + var diffJsonPatch = JsonDiff.asJson(desiredSpecNode, actualSpecNode); + for (int i = 0; i < diffJsonPatch.size(); i++) { + String operation = diffJsonPatch.get(i).get("op").asText(); + if (!operation.equals("add")) { + return false; + } + } + return true; + } +} 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 7a1a2e7afb..3b73a5e97f 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 @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; @@ -32,6 +31,7 @@ public abstract class KubernetesDependentResource informerEventSource; private boolean addOwnerReference; + protected ResourceMatcher resourceMatcher; @Override public void configureWith(KubernetesDependentResourceConfig config) { @@ -40,7 +40,7 @@ public void configureWith(KubernetesDependentResourceConfig config) { } @SuppressWarnings("unchecked") - private void configureWith(ConfigurationService service, String labelSelector, + private void configureWith(ConfigurationService configService, String labelSelector, Set namespaces, boolean addOwnerReference) { final var primaryResourcesRetriever = (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this @@ -50,26 +50,28 @@ private void configureWith(ConfigurationService service, String labelSelector, ? (AssociatedSecondaryResourceIdentifier

) this : ResourceID::fromResource; InformerConfiguration ic = - InformerConfiguration.from(service, resourceType()) + InformerConfiguration.from(configService, resourceType()) .withLabelSelector(labelSelector) .withNamespaces(namespaces) .withPrimaryResourcesRetriever(primaryResourcesRetriever) .withAssociatedSecondaryResourceIdentifier(secondaryResourceIdentifier) .build(); - this.addOwnerReference = addOwnerReference; - informerEventSource = new InformerEventSource<>(ic, client); + configureWith(configService, new InformerEventSource<>(ic, client), addOwnerReference); } /** * Use to share informers between event more resources. - * + * + * @param configurationService get configs * @param informerEventSource informer to use * @param addOwnerReference to the created resource */ - public void configureWith(InformerEventSource informerEventSource, + public void configureWith(ConfigurationService configurationService, + InformerEventSource informerEventSource, boolean addOwnerReference) { this.informerEventSource = informerEventSource; this.addOwnerReference = addOwnerReference; + initResourceMatcherIfNotSet(configurationService); } protected void beforeCreateOrUpdate(R desired, P primary) { @@ -79,8 +81,8 @@ protected void beforeCreateOrUpdate(R desired, P primary) { } @Override - protected boolean match(R actual, R target, Context context) { - return ReconcilerUtils.specsEqual(actual, target); + protected boolean match(R actualResource, R desiredResource, Context context) { + return resourceMatcher.match(actualResource, desiredResource, context); } @SuppressWarnings("unchecked") @@ -107,6 +109,7 @@ protected R update(R actual, R target, P primary, Context context) { @Override public Optional eventSource(EventSourceContext

context) { + initResourceMatcherIfNotSet(context.getConfigurationService()); if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); @@ -144,4 +147,16 @@ public Optional getResource(P primaryResource) { public void setKubernetesClient(KubernetesClient kubernetesClient) { this.client = kubernetesClient; } + + /** + * Override this method to configure resource matcher + * + * @param configurationService config service to mainly access object mapper + */ + protected void initResourceMatcherIfNotSet(ConfigurationService configurationService) { + if (resourceMatcher == null) { + resourceMatcher = new DesiredValueMatcher(configurationService.getObjectMapper()); + } + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceComparators.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceComparators.java new file mode 100644 index 0000000000..037cb597f4 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceComparators.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Secret; + +public class ResourceComparators { + + public static boolean compareConfigMapData(ConfigMap c1, ConfigMap c2) { + return Objects.equals(c1.getData(), c2.getData()) && + Objects.equals(c1.getBinaryData(), c2.getBinaryData()); + } + + public static boolean compareSecretData(Secret s1, Secret s2) { + return Objects.equals(s1.getType(), s2.getType()) && + Objects.equals(s1.getData(), s2.getData()) && + Objects.equals(s1.getStringData(), s2.getStringData()); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java new file mode 100644 index 0000000000..0d86709ca0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface ResourceMatcher { + + boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context); +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java new file mode 100644 index 0000000000..d3e013d54d --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.ReconcilerUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.assertj.core.api.Assertions.assertThat; + +class DesiredValueMatcherTest { + + DesiredValueMatcher desiredValueMatcher = new DesiredValueMatcher(new ObjectMapper()); + + @Test + void checksIfDesiredValuesAreTheSame() { + var target1 = createDeployment(); + var desired1 = createDeployment(); + assertThat(desiredValueMatcher.match(target1, desired1, null)).isTrue(); + + var target2 = createDeployment(); + var desired2 = createDeployment(); + target2.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); + assertThat(desiredValueMatcher.match(target2, desired2, null)) + .withFailMessage("Additive changes should be ok") + .isTrue(); + + var target3 = createDeployment(); + var desired3 = createDeployment(); + desired3.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); + assertThat(desiredValueMatcher.match(target3, desired3, null)) + .withFailMessage("Removed value should not be ok") + .isFalse(); + + var target4 = createDeployment(); + var desired4 = createDeployment(); + target4.getSpec().setReplicas(2); + assertThat(desiredValueMatcher.match(target4, desired4, null)) + .withFailMessage("Changed values are not ok") + .isFalse(); + } + + Deployment createDeployment() { + Deployment deployment = + ReconcilerUtils.loadYaml( + Deployment.class, DesiredValueMatcherTest.class, "nginx-deployment.yaml"); + return deployment; + } +} diff --git a/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml new file mode 100644 index 0000000000..dcf90a8fc7 --- /dev/null +++ b/operator-framework-core/src/test/resources/io/javaoperatorsdk/operator/processing/dependent/kubernetes/nginx-deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 +kind: Deployment +metadata: + name: "test" +spec: + progressDeadlineSeconds: 600 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: "test-dependent" + replicas: 1 + template: + metadata: + labels: + app: "test-dependent" + spec: + containers: + - name: nginx + image: nginx:1.17.0 + ports: + - containerPort: 80 diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index b168d8fb47..f0ac52458d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; -import java.util.Objects; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; @@ -72,14 +71,5 @@ protected Deployment desired(StandaloneDependentTestCustomResource primary, Cont deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); return deployment; } - - @Override - protected boolean match(Deployment actual, Deployment target, Context context) { - return Objects.equals(actual.getSpec().getReplicas(), target.getSpec().getReplicas()) && - actual.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() - .equals( - target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); - } - } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index a0c22ce9b1..91da4523ed 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -43,11 +43,4 @@ protected Deployment desired(Tomcat tomcat, Context context) { .build(); return deployment; } - - @Override - public boolean match(Deployment fetched, Deployment target, Context context) { - return fetched.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() - .equals(target.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); - } - } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index cc9bce6c6d..c8a39a6274 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -62,7 +62,7 @@ public UpdateControl reconcile(WebPage webPage, Context context) { WebPageStatus status = new WebPageStatus(); waitUntilConfigMapAvailable(webPage); - status.setHtmlConfigMap(configMapDR.getResource(webPage).get().getMetadata().getName()); + status.setHtmlConfigMap(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()); status.setAreWeGood("Yes!"); status.setErrorMessage(null); webPage.setStatus(status); @@ -114,12 +114,6 @@ protected Deployment desired(WebPage webPage, Context context) { return deployment; } - @Override - protected boolean match(Deployment actual, Deployment target, Context context) { - // todo comparator - return true; - } - @Override protected Class resourceType() { return Deployment.class; @@ -140,11 +134,6 @@ protected Service desired(WebPage webPage, Context context) { return service; } - protected boolean match(Service actual, Service target, Context context) { - // todo comparator - return true; - } - @Override protected Class resourceType() { return Service.class; From 6fc3b50419a29e2c908f5c17c5826dafcdf7e334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Feb 2022 08:38:18 +0100 Subject: [PATCH 0307/1608] chore(deps): bump micrometer-core from 1.8.2 to 1.8.3 (#951) Bumps [micrometer-core](https://github.com/micrometer-metrics/micrometer) from 1.8.2 to 1.8.3. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.8.2...v1.8.3) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fac0ce7e01..f8ee53d5bc 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 3.22.0 4.1.1 2.6.3 - 1.8.2 + 1.8.3 2.11 3.10.0 From 56921fe25231b9098bb2fec28ab0a6babfa50e55 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 17 Feb 2022 08:57:13 +0100 Subject: [PATCH 0308/1608] refactor: remove multiple loadYaml implementations (#950) * refactor: remove multiple loadYaml implementations * fix: provide proper class loader! :facepalm: --- .../StandaloneDependentTestReconciler.java | 16 +++------------- .../sample/DeploymentDependentResource.java | 4 +++- .../sample/ServiceDependentResource.java | 3 ++- .../operator/sample/TomcatReconciler.java | 11 ----------- .../operator/sample/WebPageReconciler.java | 16 +++------------- 5 files changed, 11 insertions(+), 39 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index f0ac52458d..c08e18cfdf 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -1,12 +1,10 @@ package io.javaoperatorsdk.operator.sample.standalonedependent; -import java.io.IOException; -import java.io.InputStream; import java.util.List; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -52,21 +50,13 @@ public KubernetesClient getKubernetesClient() { return this.kubernetesClient; } - private T loadYaml(Class clazz, String yaml) { - try (InputStream is = getClass().getResourceAsStream(yaml)) { - return Serialization.unmarshal(is, clazz); - } catch (IOException ex) { - throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); - } - } - private class DeploymentDependentResource extends KubernetesDependentResource { @Override protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { - Deployment deployment = StandaloneDependentTestReconciler.this.loadYaml(Deployment.class, - "nginx-deployment.yaml"); + Deployment deployment = + ReconcilerUtils.loadYaml(Deployment.class, getClass(), "nginx-deployment.yaml"); deployment.getMetadata().setName(primary.getMetadata().getName()); deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); return deployment; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 91da4523ed..3a68973165 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -3,6 +3,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -16,7 +17,8 @@ private static String tomcatImage(Tomcat tomcat) { @Override protected Deployment desired(Tomcat tomcat, Context context) { - Deployment deployment = TomcatReconciler.loadYaml(Deployment.class, "deployment.yaml"); + Deployment deployment = + ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); final String tomcatName = tomcatMetadata.getName(); deployment = new DeploymentBuilder(deployment) diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index df690f6abd..a77d13b345 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -3,6 +3,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -11,7 +12,7 @@ public class ServiceDependentResource extends KubernetesDependentResource T loadYaml(Class clazz, String yaml) { - try (InputStream is = TomcatReconciler.class.getResourceAsStream(yaml)) { - return Serialization.unmarshal(is, clazz); - } catch (IOException ex) { - throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); - } - } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index c8a39a6274..52dc44ebed 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.sample; -import java.io.IOException; -import java.io.InputStream; import java.time.Duration; import java.util.*; @@ -12,7 +10,6 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.utils.Serialization; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @@ -20,6 +17,7 @@ import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; import static org.awaitility.Awaitility.await; @@ -92,7 +90,7 @@ private void createDependentResources(KubernetesClient client) { @Override protected Deployment desired(WebPage webPage, Context context) { var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, "deployment.yaml"); + Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); deployment.getMetadata().setName(deploymentName); deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); @@ -125,7 +123,7 @@ protected Class resourceType() { @Override protected Service desired(WebPage webPage, Context context) { - Service service = loadYaml(Service.class, "service.yaml"); + Service service = loadYaml(Service.class, getClass(), "service.yaml"); service.getMetadata().setName(serviceName(webPage)); service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); Map labels = new HashMap<>(); @@ -153,14 +151,6 @@ private static String serviceName(WebPage nginx) { return nginx.getMetadata().getName(); } - private T loadYaml(Class clazz, String yaml) { - try (InputStream is = getClass().getResourceAsStream(yaml)) { - return Serialization.unmarshal(is, clazz); - } catch (IOException ex) { - throw new IllegalStateException("Cannot find yaml on classpath: " + yaml); - } - } - private class ConfigMapDependentResource extends KubernetesDependentResource implements AssociatedSecondaryResourceIdentifier { From 7d1ecca996f64df90e3aecfffcce4277642d34f1 Mon Sep 17 00:00:00 2001 From: Rishi Kumar Ray <87641376+RishiKumarRay@users.noreply.github.com> Date: Thu, 17 Feb 2022 15:01:57 +0530 Subject: [PATCH 0309/1608] Tweet on release! (#913) --- .github/workflows/release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b36caff8f1..d250e8610a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,21 @@ jobs: gpg_passphrase: ${{ secrets.GPG_PASSPHRASE }} nexus_username: ${{ secrets.OSSRH_USERNAME }} nexus_password: ${{ secrets.OSSRH_TOKEN }} + - uses: Eomm/why-don-t-you-tweet@v1 + # We don't want to tweet if the repository is not a public one + if: ${{ !github.event.repository.private }} + with: + # GitHub event payload + # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release + tweet-message: "New ${{ github.event.repository.name }} release ${{ github.event.release.tag_name }}! Try it will it is HOT! ${{ github.event.release.html_url }} #release" + env: + # Get your tokens from https://developer.twitter.com/apps + TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} + TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + + # This is separate job because there were issues with git after release step, was not able to commit changes. See history. update-working-version: From 73dc44543ea5bdf8d6185afee3e98639913d98f7 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 17 Feb 2022 11:17:44 +0100 Subject: [PATCH 0310/1608] feat: extract interfaces for optional behavior of DependentResource (#949) * feat: extract EventSourceProvider interface to clean up common use case * feat: extract DependentResourceConfigurator interface * feat: showcase configuration change --- .../io/javaoperatorsdk/operator/Operator.java | 5 +++-- .../ControllerConfigurationOverrider.java | 5 +++-- .../dependent/DependentResourceSpec.java | 2 +- .../dependent/AbstractDependentResource.java | 2 +- .../dependent/DependentResource.java | 9 +------- .../DependentResourceConfigurator.java | 5 +++++ .../dependent/EventSourceProvider.java | 10 +++++++++ .../dependent/DependentResourceManager.java | 21 ++++++++++++------- .../KubernetesDependentResource.java | 11 ++++++---- .../StandaloneDependentTestReconciler.java | 2 +- .../operator/sample/MySQLSchemaOperator.java | 3 ++- .../sample/SchemaDependentResource.java | 14 ++++++++----- .../operator/sample/WebPageReconciler.java | 9 ++++---- 13 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java 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 2b15ab5cda..adc3410405 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 @@ -157,7 +157,7 @@ public void register(Reconciler reconciler, * @param the {@code HasMetadata} type associated with the reconciler */ public void register(Reconciler reconciler, - Consumer configOverrider) { + Consumer> configOverrider) { final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); var configToOverride = ControllerConfigurationOverrider.override(controllerConfiguration); configOverrider.accept(configToOverride); @@ -195,7 +195,8 @@ public synchronized void stop() { started = false; } - public synchronized void add(Controller controller) { + @SuppressWarnings("unchecked") + synchronized void add(Controller controller) { final var configuration = controller.getConfiguration(); final var resourceTypeName = ReconcilerUtils .getResourceTypeNameWithVersion(configuration.getResourceClass()); 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 7fb919abb1..239265af12 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 @@ -11,6 +11,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +@SuppressWarnings({"rawtypes", "unchecked", "unused"}) public class ControllerConfigurationOverrider { private String finalizer; @@ -21,7 +22,7 @@ public class ControllerConfigurationOverrider { private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private Duration reconciliationMaxInterval; - private List dependentResourceSpecs; + private final List dependentResourceSpecs; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizer(); @@ -89,7 +90,7 @@ public ControllerConfigurationOverrider withReconciliationMaxInterval( } public void replaceDependentResourceConfig( - Class> dependentResourceClass, + Class> dependentResourceClass, Object dependentResourceConfig) { var currentConfig = 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 aea90b5e36..1c07a5c752 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 @@ -4,7 +4,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -public class DependentResourceSpec, C> { +public class DependentResourceSpec, C> { private final Class dependentResourceClass; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index beac9ef4bb..bf40654812 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -4,7 +4,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; public abstract class AbstractDependentResource - implements DependentResource { + implements DependentResource { @Override public void reconcile(P primary, Context context) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 5208480166..fff45fa50f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -4,18 +4,11 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -public interface DependentResource { - - Optional eventSource(EventSourceContext

context); +public interface DependentResource { void reconcile(P primary, Context context); void delete(P primary, Context context); Optional getResource(P primaryResource); - - default void configureWith(C config) {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java new file mode 100644 index 0000000000..b51de9f35f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java @@ -0,0 +1,5 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +public interface DependentResourceConfigurator { + void configureWith(C config); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java new file mode 100644 index 0000000000..c9a2ca35a2 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public interface EventSourceProvider

{ + + EventSource eventSource(EventSourceContext

context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 102ffef088..e860cbe577 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -21,6 +21,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.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -49,11 +51,11 @@ public List prepareEventSources(EventSourceContext

context) { dependentResources.stream() .map( drc -> { - final var dependentResource = - createAndConfigureFrom(drc, context.getClient()); - dependentResource - .eventSource(context) - .ifPresent(es -> sources.add((EventSource) es)); + final var dependentResource = createAndConfigureFrom(drc, context.getClient()); + if (dependentResource instanceof EventSourceProvider) { + EventSourceProvider provider = (EventSourceProvider) dependentResource; + sources.add(provider.eventSource(context)); + } return dependentResource; }) .collect(Collectors.toList()); @@ -87,11 +89,16 @@ private DependentResource createAndConfigureFrom(DependentResourceSpec dependent DependentResource dependentResource = (DependentResource) dependentResourceSpec.getDependentResourceClass() .getConstructor().newInstance(); + if (dependentResource instanceof KubernetesClientAware) { ((KubernetesClientAware) dependentResource).setKubernetesClient(client); } - dependentResourceSpec.getDependentResourceConfiguration() - .ifPresent(dependentResource::configureWith); + + if (dependentResource instanceof DependentResourceConfigurator) { + final var configurator = (DependentResourceConfigurator) dependentResource; + dependentResourceSpec.getDependentResourceConfiguration() + .ifPresent(configurator::configureWith); + } return dependentResource; } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 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 3b73a5e97f..182ff9b361 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 @@ -14,6 +14,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -24,7 +26,8 @@ public abstract class KubernetesDependentResource extends AbstractDependentResource - implements KubernetesClientAware { + implements KubernetesClientAware, EventSourceProvider

, + DependentResourceConfigurator { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); @@ -61,7 +64,7 @@ private void configureWith(ConfigurationService configService, String labelSelec /** * Use to share informers between event more resources. - * + * * @param configurationService get configs * @param informerEventSource informer to use * @param addOwnerReference to the created resource @@ -108,7 +111,7 @@ protected R update(R actual, R target, P primary, Context context) { } @Override - public Optional eventSource(EventSourceContext

context) { + public EventSource eventSource(EventSourceContext

context) { initResourceMatcherIfNotSet(context.getConfigurationService()); if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, @@ -116,7 +119,7 @@ public Optional eventSource(EventSourceContext

context) { log.warn("Using default configuration for " + resourceType().getSimpleName() + " KubernetesDependentResource, call configureWith to provide configuration"); } - return Optional.of(informerEventSource); + return informerEventSource; } public KubernetesDependentResource setInformerEventSource( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index c08e18cfdf..d529f19c3d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -29,7 +29,7 @@ public StandaloneDependentTestReconciler() { @Override public List prepareEventSources( EventSourceContext context) { - return List.of(configMapDependent.eventSource(context).get()); + return List.of(configMapDependent.eventSource(context)); } @Override 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 5a39408ec8..150c72b391 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 @@ -35,10 +35,11 @@ public static void main(String[] args) throws IOException { MySQLSchemaReconciler schemaReconciler = new MySQLSchemaReconciler(); + // override the default configuration operator.register(schemaReconciler, configOverrider -> configOverrider.replaceDependentResourceConfig( SchemaDependentResource.class, - new ResourcePollerConfig(500, MySQLDbConfig.loadFromEnvironmentVars()))); + new ResourcePollerConfig(300, MySQLDbConfig.loadFromEnvironmentVars()))); operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 338a420b65..67c553268f 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -8,6 +8,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; import io.javaoperatorsdk.operator.sample.schema.Schema; @@ -15,8 +17,10 @@ import static java.lang.String.format; -public class SchemaDependentResource extends - AbstractDependentResource { +public class SchemaDependentResource + extends AbstractDependentResource + implements EventSourceProvider, + DependentResourceConfigurator { private MySQLDbConfig dbConfig; private int pollPeriod = 500; @@ -28,13 +32,13 @@ public void configureWith(ResourcePollerConfig config) { } @Override - public Optional eventSource(EventSourceContext context) { + public EventSource eventSource(EventSourceContext context) { if (dbConfig == null) { dbConfig = MySQLDbConfig.loadFromEnvironmentVars(); } - return Optional.of(new PerResourcePollingEventSource<>( + return new PerResourcePollingEventSource<>( new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), pollPeriod, - Schema.class)); + Schema.class); } @Override diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 52dc44ebed..b3200903d1 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -40,11 +40,10 @@ public WebPageReconciler(KubernetesClient kubernetesClient) { @Override public List prepareEventSources(EventSourceContext context) { - List eventSources = new ArrayList<>(3); - configMapDR.eventSource(context).ifPresent(eventSources::add); - deploymentDR.eventSource(context).ifPresent(eventSources::add); - serviceDR.eventSource(context).ifPresent(eventSources::add); - return eventSources; + return List.of( + configMapDR.eventSource(context), + deploymentDR.eventSource(context), + serviceDR.eventSource(context)); } @Override From c03bb77af7e2ac88af6fecccaf07e071d4d1acdb Mon Sep 17 00:00:00 2001 From: Matteo Mortari Date: Mon, 21 Feb 2022 08:48:50 +0100 Subject: [PATCH 0311/1608] fix README instructions for webpage sample (#959) k8s/crd.yml no longer exists as the CRDs for Samples are automatically generated by the fabric8 maven plugin --- sample-operators/webpage/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sample-operators/webpage/README.md b/sample-operators/webpage/README.md index ba17b7d962..8cc20a40f8 100644 --- a/sample-operators/webpage/README.md +++ b/sample-operators/webpage/README.md @@ -28,7 +28,11 @@ spec: The quickest way to try the operator is to run it on your local machine, while it connects to a local or remote Kubernetes cluster. When you start it, it will use the current kubectl context on your machine to connect to the cluster. -Before you run it you have to install the CRD on your cluster by running `kubectl apply -f k8s/crd.yaml` +Before you run it you have to install the CRD on your cluster by running +`kubectl apply -f target/classes/META-INF/fabric8/webpages.sample.javaoperatorsdk-v1.yml`. + +The CRD is generated automatically from your code by simply adding the `crd-generator-apt` +dependency to your `pom.xml` file. When the Operator is running you can create some Webserver Custom Resources. You can find a sample custom resource in `k8s/webpage.yaml`. You can create it by running `kubectl apply -f k8s/webpage.yaml` @@ -42,6 +46,8 @@ page. Otherwise you can change the service to a LoadBalancer (e.g on a public cl You can also try to change the HTML code in `k8s/webpage.yaml` and do another `kubectl apply -f k8s/webpage.yaml`. This should update the actual NGINX deployment with the new configuration. +If you want the Operator to be running as a deployment in your cluster, follow the below steps. + ### Build You can build the sample using `mvn jib:dockerBuild` this will produce a Docker image you can push to the registry @@ -49,5 +55,5 @@ of your choice. The JAR file is built using your local Maven and JDK and then co ### Deployment -1. Deploy the CRD: `kubectl apply -f k8s/crd.yaml` +1. Deploy the CRD: `kubectl apply -f target/classes/META-INF/fabric8/webpages.sample.javaoperatorsdk-v1.yml` 2. Deploy the operator: `kubectl apply -f k8s/operator.yaml` From 2832fc2eab3de6ebcee5c8f705b68f479fa87a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 21 Feb 2022 15:41:08 +0100 Subject: [PATCH 0312/1608] feature: update preserves metadata (#960) --- .../operator/ReconcilerUtils.java | 9 ++++ .../KubernetesDependentResource.java | 30 ++++++++++---- .../ResourceUpdatePreProcessor.java | 33 +++++++++++++++ .../operator/ReconcilerUtilsTest.java | 13 ++++++ .../ResourceUpdatePreProcessorTest.java | 41 +++++++++++++++++++ 5 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 491910d13e..98a43492f9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -118,6 +118,15 @@ public static Object getSpec(HasMetadata resource) { } } + public static Object setSpec(HasMetadata resource, Object spec) { + try { + Method setSpecMethod = resource.getClass().getMethod("setSpec", spec.getClass()); + return setSpecMethod.invoke(resource, spec); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException("No spec found on resource", e); + } + } + public static T loadYaml(Class clazz, Class loader, String yaml) { try (InputStream is = loader.getResourceAsStream(yaml)) { return Serialization.unmarshal(is, clazz); 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 182ff9b361..1204a3b844 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 @@ -35,6 +35,7 @@ public abstract class KubernetesDependentResource informerEventSource; private boolean addOwnerReference; protected ResourceMatcher resourceMatcher; + protected ResourceUpdatePreProcessor resourceUpdatePreProcessor; @Override public void configureWith(KubernetesDependentResourceConfig config) { @@ -74,10 +75,10 @@ public void configureWith(ConfigurationService configurationService, boolean addOwnerReference) { this.informerEventSource = informerEventSource; this.addOwnerReference = addOwnerReference; - initResourceMatcherIfNotSet(configurationService); + initResourceMatcherAndUpdatePreProcessorIfNotSet(configurationService); } - protected void beforeCreateOrUpdate(R desired, P primary) { + protected void beforeCreate(R desired, P primary) { if (addOwnerReference) { desired.addOwnerReference(primary); } @@ -93,7 +94,7 @@ protected boolean match(R actualResource, R desiredResource, Context context) { protected R create(R target, P primary, Context context) { log.debug("Creating target resource with type: " + "{}, with id: {}", target.getClass(), ResourceID.fromResource(target)); - beforeCreateOrUpdate(target, primary); + beforeCreate(target, primary); Class targetClass = (Class) target.getClass(); return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) .create(target); @@ -104,15 +105,15 @@ protected R create(R target, P primary, Context context) { protected R update(R actual, R target, P primary, Context context) { log.debug("Updating target resource with type: {}, with id: {}", target.getClass(), ResourceID.fromResource(target)); - beforeCreateOrUpdate(target, primary); Class targetClass = (Class) target.getClass(); + var updatedActual = resourceUpdatePreProcessor.replaceSpecOnActual(actual, target); return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) - .replace(target); + .replace(updatedActual); } @Override public EventSource eventSource(EventSourceContext

context) { - initResourceMatcherIfNotSet(context.getConfigurationService()); + initResourceMatcherAndUpdatePreProcessorIfNotSet(context.getConfigurationService()); if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); @@ -156,10 +157,25 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { * * @param configurationService config service to mainly access object mapper */ - protected void initResourceMatcherIfNotSet(ConfigurationService configurationService) { + protected void initResourceMatcherAndUpdatePreProcessorIfNotSet( + ConfigurationService configurationService) { if (resourceMatcher == null) { resourceMatcher = new DesiredValueMatcher(configurationService.getObjectMapper()); } + if (resourceUpdatePreProcessor == null) { + resourceUpdatePreProcessor = + new ResourceUpdatePreProcessor<>(configurationService.getResourceCloner()); + } + } + + public KubernetesDependentResource setResourceMatcher(ResourceMatcher resourceMatcher) { + this.resourceMatcher = resourceMatcher; + return this; } + public KubernetesDependentResource setResourceUpdatePreProcessor( + ResourceUpdatePreProcessor resourceUpdatePreProcessor) { + this.resourceUpdatePreProcessor = resourceUpdatePreProcessor; + return this; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java new file mode 100644 index 0000000000..c7a16381a9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java @@ -0,0 +1,33 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.Cloner; + +public class ResourceUpdatePreProcessor { + + private final Cloner cloner; + + public ResourceUpdatePreProcessor(Cloner cloner) { + this.cloner = cloner; + } + + public R replaceSpecOnActual(R actual, R desired) { + var clonedActual = cloner.clone(actual); + if (desired instanceof ConfigMap) { + ((ConfigMap) clonedActual).setData(((ConfigMap) desired).getData()); + ((ConfigMap) clonedActual).setBinaryData((((ConfigMap) desired).getBinaryData())); + return clonedActual; + } else if (desired instanceof Secret) { + ((Secret) clonedActual).setData(((Secret) desired).getData()); + ((Secret) clonedActual).setStringData(((Secret) desired).getStringData()); + return clonedActual; + } else { + var desiredSpec = ReconcilerUtils.getSpec(desired); + ReconcilerUtils.setSpec(clonedActual, desiredSpec); + return clonedActual; + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index be82caa36b..e137a2ff11 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -72,6 +72,19 @@ void getsSpecWithReflection() { assertThat(spec.getReplicas()).isEqualTo(5); } + @Test + void setsSpecWithReflection() { + Deployment deployment = new Deployment(); + deployment.setSpec(new DeploymentSpec()); + deployment.getSpec().setReplicas(5); + DeploymentSpec newSpec = new DeploymentSpec(); + newSpec.setReplicas(1); + + ReconcilerUtils.setSpec(deployment, newSpec); + + assertThat(deployment.getSpec().getReplicas()).isEqualTo(1); + } + private Deployment createTestDeployment() { Deployment deployment = new Deployment(); deployment.setSpec(new DeploymentSpec()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java new file mode 100644 index 0000000000..a4907f13b7 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; + +import static org.assertj.core.api.Assertions.assertThat; + +class ResourceUpdatePreProcessorTest { + + ResourceUpdatePreProcessor resourceUpdatePreProcessor = + new ResourceUpdatePreProcessor<>(ConfigurationService.DEFAULT_CLONER); + + @Test + void preservesValues() { + var desired = createDeployment(); + var actual = createDeployment(); + actual.getMetadata().setLabels(new HashMap<>()); + actual.getMetadata().getLabels().put("additionalActualKey", "value"); + actual.getMetadata().setResourceVersion("1234"); + actual.getSpec().setRevisionHistoryLimit(5); + + var result = resourceUpdatePreProcessor.replaceSpecOnActual(actual, desired); + + assertThat(result.getMetadata().getLabels().get("additionalActualKey")).isEqualTo("value"); + assertThat(result.getMetadata().getResourceVersion()).isEqualTo("1234"); + assertThat(result.getSpec().getRevisionHistoryLimit()).isEqualTo(10); + } + + Deployment createDeployment() { + Deployment deployment = + ReconcilerUtils.loadYaml( + Deployment.class, ResourceUpdatePreProcessorTest.class, "nginx-deployment.yaml"); + return deployment; + } + +} From 776ea2186c760476fdf352f35533c8b57e2b53e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 21 Feb 2022 15:41:18 +0100 Subject: [PATCH 0313/1608] fix: no sonar fix experiment (#962) * fix: no sonar fix experiment * Update sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java Co-authored-by: Chris Laprun * fix: format security issue Co-authored-by: Chris Laprun --- .../operator/sample/MySQLSchemaReconciler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 8965ae2a89..5952a1508f 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -43,8 +43,9 @@ public MySQLSchemaReconciler() {} @Override public void initContext(MySQLSchema primary, Context context) { final var name = primary.getMetadata().getName(); - // NOSONAR we don't need cryptographically-strong randomness here - final var password = RandomStringUtils.randomAlphanumeric(16); + final var password = RandomStringUtils + .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here + final var secretName = String.format(SECRET_FORMAT, name); final var userName = String.format(USERNAME_FORMAT, name); From 0c7110af8db1be7eb6949c562d3189e522c4b3b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Feb 2022 08:36:49 +0100 Subject: [PATCH 0314/1608] chore(deps): bump nexus-staging-maven-plugin from 1.6.11 to 1.6.12 (#967) Bumps nexus-staging-maven-plugin from 1.6.11 to 1.6.12. --- updated-dependencies: - dependency-name: org.sonatype.plugins:nexus-staging-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f8ee53d5bc..420bf65c12 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 3.2.2 3.1.0 3.0.1 - 1.6.11 + 1.6.12 2.8.2 2.5.2 5.0.0 From 2aa50648a91b620e7bbc2b59a556c52e41679c56 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Wed, 23 Feb 2022 08:42:11 +0000 Subject: [PATCH 0315/1608] Utility around curl in Cluster (#966) --- .../operator/junit/InClusterCurl.java | 54 +++++++++++++++++++ .../operator/sample/TomcatOperatorE2E.java | 33 ++---------- 2 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java new file mode 100644 index 0000000000..927723209c --- /dev/null +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.junit; + +import java.util.UUID; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.awaitility.Awaitility.await; + +public class InClusterCurl { + + private final KubernetesClient client; + private final String namespace; + + public InClusterCurl(https://github.com/KubernetesClient%20client,%20String%20namespace) { + this.client = client; + this.namespace = namespace; + } + + public String checkUrl(String url) { + String podName = KubernetesResourceUtil.sanitizeName("curl-" + UUID.randomUUID()); + try { + Pod curlPod = client.run().inNamespace(namespace) + .withRunConfig(new RunConfigBuilder() + .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) + .withName(podName) + .withImage("curlimages/curl:7.78.0") + .withRestartPolicy("Never") + .build()) + .done(); + await("wait-for-curl-pod-run").atMost(2, MINUTES) + .until(() -> { + String phase = + client.pods().inNamespace(namespace).withName(podName).get() + .getStatus().getPhase(); + return phase.equals("Succeeded") || phase.equals("Failed"); + }); + + String curlOutput = + client.pods().inNamespace(namespace) + .withName(curlPod.getMetadata().getName()).getLog(); + + return curlOutput; + } finally { + client.pods().inNamespace(namespace).withName(podName).delete(); + await("wait-for-curl-pod-stop").atMost(1, MINUTES) + .until(() -> client.pods().inNamespace(namespace).withName(podName) + .get() == null); + } + } +} diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index 042f4973cf..3fecdb710e 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -10,10 +10,10 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.*; -import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; +import io.javaoperatorsdk.operator.junit.InClusterCurl; import io.javaoperatorsdk.operator.junit.OperatorExtension; import static java.util.concurrent.TimeUnit.MINUTES; @@ -107,42 +107,15 @@ public void test() { String url = "http://" + tomcat.getMetadata().getName() + "/" + webapp1.getSpec().getContextPath() + "/"; + var inClusterCurl = new InClusterCurl(https://github.com/client,%20operator.getNamespace()); log.info("Starting curl Pod and waiting 5 minutes for GET of {} to return 200", url); await("wait-for-webapp").atMost(6, MINUTES).untilAsserted(() -> { try { - - log.info("Starting curl Pod to test if webapp was deployed correctly"); - Pod curlPod = client.run().inNamespace(operator.getNamespace()) - .withRunConfig(new RunConfigBuilder() - .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) - .withName("curl") - .withImage("curlimages/curl:7.78.0") - .withRestartPolicy("Never") - .build()) - .done(); - log.info("Waiting for curl Pod to finish running"); - await("wait-for-curl-pod-run").atMost(2, MINUTES) - .until(() -> { - String phase = - client.pods().inNamespace(operator.getNamespace()).withName("curl").get() - .getStatus().getPhase(); - return phase.equals("Succeeded") || phase.equals("Failed"); - }); - - String curlOutput = - client.pods().inNamespace(operator.getNamespace()) - .withName(curlPod.getMetadata().getName()).getLog(); - log.info("Output from curl: '{}'", curlOutput); + var curlOutput = inClusterCurl.checkUrl(url); assertThat(curlOutput, equalTo("200")); } catch (KubernetesClientException ex) { throw new AssertionError(ex); - } finally { - log.info("Deleting curl Pod"); - client.pods().inNamespace(operator.getNamespace()).withName("curl").delete(); - await("wait-for-curl-pod-stop").atMost(1, MINUTES) - .until(() -> client.pods().inNamespace(operator.getNamespace()).withName("curl") - .get() == null); } }); } From ae4ad56d7dd5be43ee04127a6e6d33401e5bbfcb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 24 Feb 2022 12:56:59 +0100 Subject: [PATCH 0316/1608] feat: dependents use traits to specify which features they support (#963) Also fixes an issue with dependent spec creation. --- .../operator/api/reconciler/Context.java | 4 + .../api/reconciler/DefaultContext.java | 8 ++ .../dependent/AbstractDependentResource.java | 82 +++++++++++--- .../api/reconciler/dependent/Creator.java | 12 ++ .../api/reconciler/dependent/Deleter.java | 12 ++ .../dependent/DependentResource.java | 2 +- .../api/reconciler/dependent/Matcher.java | 7 ++ .../dependent/ResourceUpdatePreProcessor.java | 9 ++ .../api/reconciler/dependent/Updater.java | 18 +++ ... => GenericKubernetesResourceMatcher.java} | 31 ++--- .../GenericResourceUpdatePreProcessor.java | 53 +++++++++ .../KubernetesDependentResource.java | 107 +++++++----------- .../dependent/kubernetes/ResourceMatcher.java | 9 -- .../ResourceUpdatePreProcessor.java | 33 ------ .../source/informer/InformerEventSource.java | 10 -- ...GenericKubernetesResourceMatcherTest.java} | 28 +++-- ...enericResourceUpdatePreProcessorTest.java} | 27 +++-- .../AnnotationControllerConfiguration.java | 66 +++++------ ...AnnotationControllerConfigurationTest.java | 58 ++++++++++ .../sample/readonly/ConfigMapReader.java | 11 ++ .../sample/readonly/ReadOnlyDependent.java | 7 ++ .../StandaloneDependentTestReconciler.java | 16 +-- .../sample/SchemaDependentResource.java | 21 ++-- .../sample/SecretDependentResource.java | 13 +-- .../sample/DeploymentDependentResource.java | 5 +- .../sample/ServiceDependentResource.java | 5 +- .../operator/sample/WebPageReconciler.java | 11 +- 27 files changed, 430 insertions(+), 235 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/{DesiredValueMatcher.java => GenericKubernetesResourceMatcher.java} (50%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/{DesiredValueMatcherTest.java => GenericKubernetesResourceMatcherTest.java} (53%) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/{ResourceUpdatePreProcessorTest.java => GenericResourceUpdatePreProcessorTest.java} (51%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ConfigMapReader.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 6870fef80e..6948d0f6a7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -2,6 +2,8 @@ import java.util.Optional; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; + public interface Context extends AttributeHolder { Optional getRetryInfo(); @@ -18,4 +20,6 @@ default T getMandatory(Object key, Class expectedType) { "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + ") is missing or not of the expected type")); } + + ConfigurationService getConfigurationService(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index c2553a5d57..46ee5a3fcf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.processing.Controller; public class DefaultContext

extends MapAttributeHolder implements Context { @@ -10,11 +11,13 @@ public class DefaultContext

extends MapAttributeHolder im private final RetryInfo retryInfo; private final Controller

controller; private final P primaryResource; + private final ConfigurationService configurationService; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; + this.configurationService = controller.getConfiguration().getConfigurationService(); } @Override @@ -28,4 +31,9 @@ public Optional getSecondaryResource(Class expectedType, String eventS .getResourceEventSourceFor(expectedType, eventSourceName) .flatMap(es -> es.getAssociated(primaryResource)); } + + @Override + public ConfigurationService getConfigurationService() { + return configurationService; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index bf40654812..456e9339de 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -1,31 +1,87 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -public abstract class AbstractDependentResource +public abstract class AbstractDependentResource implements DependentResource { + private static final Logger log = LoggerFactory.getLogger(AbstractDependentResource.class); + + 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; + protected Deleter

deleter; + + @SuppressWarnings("unchecked") + public AbstractDependentResource() { + init(Creator.NOOP, Updater.NOOP, Deleter.NOOP); + } + + @SuppressWarnings({"unchecked"}) + protected void init(Creator defaultCreator, Updater defaultUpdater, + Deleter

defaultDeleter) { + creator = creatable ? (Creator) this : defaultCreator; + updater = updatable ? (Updater) this : defaultUpdater; + deleter = deletable ? (Deleter

) this : defaultDeleter; + } @Override public void reconcile(P primary, Context context) { - var actual = getResource(primary); - var desired = desired(primary, context); - if (actual.isEmpty()) { - create(desired, primary, context); - } else { - if (!match(actual.get(), desired, context)) { - update(actual.get(), desired, primary, context); + final var creatable = isCreatable(primary, context); + final var updatable = isUpdatable(primary, context); + if (creatable || updatable) { + var maybeActual = getResource(primary); + var desired = desired(primary, context); + if (maybeActual.isEmpty()) { + if (creatable) { + log.debug("Creating dependent {} for primary {}", desired, primary); + creator.create(desired, primary, context); + } + } else { + final var actual = maybeActual.get(); + if (updatable && !updater.match(actual, desired, context)) { + log.debug("Updating dependent {} for primary {}", desired, primary); + updater.update(actual, desired, primary, context); + } else { + log.debug("Update skipped for dependent {} as it matched the existing one", desired); + } } + } else { + log.debug( + "Dependent {} is read-only, implement Creator and/or Updater interfaces to modify it", + getClass().getSimpleName()); } } - protected abstract R desired(P primary, Context context); + @Override + public void delete(P primary, Context context) { + if (isDeletable(primary, context)) { + deleter.delete(primary, context); + } + } - protected abstract boolean match(R actual, R target, Context context); + protected R desired(P primary, Context context) { + throw new IllegalStateException( + "desired method must be implemented if this DependentResource can be created and/or updated"); + } - protected abstract R create(R target, P primary, Context context); + @SuppressWarnings("unused") + protected boolean isCreatable(P primary, Context context) { + return creatable; + } - // the actual needed to copy/preserve new labels or annotations - protected abstract R update(R actual, R target, P primary, Context context); + @SuppressWarnings("unused") + protected boolean isUpdatable(P primary, Context context) { + return updatable; + } + @SuppressWarnings("unused") + protected boolean isDeletable(P primary, Context context) { + return deletable; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java new file mode 100644 index 0000000000..f685a5edb4 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@SuppressWarnings("rawtypes") +public interface Creator { + Creator NOOP = (desired, primary, context) -> { + }; + + void create(R desired, P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java new file mode 100644 index 0000000000..7649460934 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@SuppressWarnings("rawtypes") +public interface Deleter

{ + Deleter NOOP = (primary, context) -> { + }; + + void delete(P primary, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index fff45fa50f..09394f11b0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -8,7 +8,7 @@ public interface DependentResource { void reconcile(P primary, Context context); - void delete(P primary, Context context); + default void delete(P primary, Context context) {} Optional getResource(P primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java new file mode 100644 index 0000000000..ad82daf661 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface Matcher { + boolean match(R actualResource, R desiredResource, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java new file mode 100644 index 0000000000..df2541aae9 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java @@ -0,0 +1,9 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public interface ResourceUpdatePreProcessor { + + R replaceSpecOnActual(R actual, R desired, Context context); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java new file mode 100644 index 0000000000..6984f788f8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Objects; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +@SuppressWarnings("rawtypes") +public interface Updater { + Updater NOOP = (actual, desired, primary, context) -> { + }; + + void update(R actual, R desired, P primary, Context context); + + default boolean match(R actualResource, R desiredResource, Context context) { + return Objects.equals(actualResource, desiredResource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java similarity index 50% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index b8c9b1c7a5..1c3c7d0822 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -6,27 +6,28 @@ import io.fabric8.zjsonpatch.JsonDiff; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; -import com.fasterxml.jackson.databind.ObjectMapper; +public class GenericKubernetesResourceMatcher implements Matcher { -public class DesiredValueMatcher implements ResourceMatcher { + private GenericKubernetesResourceMatcher() {} - private final ObjectMapper objectMapper; - - public DesiredValueMatcher(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Matcher matcherFor(Class resourceType) { + if (Secret.class.isAssignableFrom(resourceType)) { + return (actual, desired, context) -> ResourceComparators.compareSecretData((Secret) desired, + (Secret) actual); + } else if (ConfigMap.class.isAssignableFrom(resourceType)) { + return (actual, desired, context) -> ResourceComparators + .compareConfigMapData((ConfigMap) desired, (ConfigMap) actual); + } else { + return new GenericKubernetesResourceMatcher(); + } } @Override - public boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context) { - if (actualResource instanceof Secret) { - return ResourceComparators.compareSecretData((Secret) desiredResource, - (Secret) actualResource); - } - if (actualResource instanceof ConfigMap) { - return ResourceComparators.compareConfigMapData((ConfigMap) desiredResource, - (ConfigMap) actualResource); - } + public boolean match(R actualResource, R desiredResource, Context context) { + final var objectMapper = context.getConfigurationService().getObjectMapper(); // reflection will be replaced by this: // https://github.com/fabric8io/kubernetes-client/issues/3816 var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desiredResource)); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java new file mode 100644 index 0000000000..b33719f64e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; + +public abstract class GenericResourceUpdatePreProcessor implements + ResourceUpdatePreProcessor { + + private GenericResourceUpdatePreProcessor() {} + + @SuppressWarnings("unchecked") + public static ResourceUpdatePreProcessor processorFor( + Class resourceType) { + if (Secret.class.isAssignableFrom(resourceType)) { + return (ResourceUpdatePreProcessor) new GenericResourceUpdatePreProcessor() { + @Override + protected void updateClonedActual(Secret actual, Secret desired) { + actual.setData(desired.getData()); + actual.setStringData(desired.getStringData()); + } + }; + } else if (ConfigMap.class.isAssignableFrom(resourceType)) { + return (ResourceUpdatePreProcessor) new GenericResourceUpdatePreProcessor() { + + @Override + protected void updateClonedActual(ConfigMap actual, ConfigMap desired) { + actual.setData(desired.getData()); + actual.setBinaryData((desired.getBinaryData())); + } + }; + } else { + return new GenericResourceUpdatePreProcessor<>() { + @Override + protected void updateClonedActual(R actual, R desired) { + var desiredSpec = ReconcilerUtils.getSpec(desired); + ReconcilerUtils.setSpec(actual, desiredSpec); + } + }; + } + } + + public R replaceSpecOnActual(R actual, R desired, Context context) { + var clonedActual = context.getConfigurationService().getResourceCloner().clone(actual); + updateClonedActual(clonedActual, desired); + return clonedActual; + } + + protected abstract void updateClonedActual(R actual, R desired); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 1204a3b844..a0513b1234 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 @@ -7,7 +7,10 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; +import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; @@ -17,6 +20,8 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -25,7 +30,7 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; public abstract class KubernetesDependentResource - extends AbstractDependentResource + extends AbstractDependentResource implements KubernetesClientAware, EventSourceProvider

, DependentResourceConfigurator { @@ -34,8 +39,18 @@ public abstract class KubernetesDependentResource informerEventSource; private boolean addOwnerReference; - protected ResourceMatcher resourceMatcher; - protected ResourceUpdatePreProcessor resourceUpdatePreProcessor; + private final Matcher matcher; + private final ResourceUpdatePreProcessor processor; + + @SuppressWarnings("unchecked") + public KubernetesDependentResource() { + init(this::create, this::update, this::delete); + matcher = this instanceof Matcher ? (Matcher) this + : GenericKubernetesResourceMatcher.matcherFor(resourceType()); + processor = this instanceof ResourceUpdatePreProcessor + ? (ResourceUpdatePreProcessor) this + : GenericResourceUpdatePreProcessor.processorFor(resourceType()); + } @Override public void configureWith(KubernetesDependentResourceConfig config) { @@ -75,45 +90,44 @@ public void configureWith(ConfigurationService configurationService, boolean addOwnerReference) { this.informerEventSource = informerEventSource; this.addOwnerReference = addOwnerReference; - initResourceMatcherAndUpdatePreProcessorIfNotSet(configurationService); } - protected void beforeCreate(R desired, P primary) { - if (addOwnerReference) { - desired.addOwnerReference(primary); - } + public void create(R target, P primary, Context context) { + prepare(target, primary, "Creating").create(target); } - @Override - protected boolean match(R actualResource, R desiredResource, Context context) { - return resourceMatcher.match(actualResource, desiredResource, context); + public void update(R actual, R target, P primary, Context context) { + var updatedActual = processor.replaceSpecOnActual(actual, target, context); + prepare(target, primary, "Updating").replace(updatedActual); } - @SuppressWarnings("unchecked") - @Override - protected R create(R target, P primary, Context context) { - log.debug("Creating target resource with type: " + - "{}, with id: {}", target.getClass(), ResourceID.fromResource(target)); - beforeCreate(target, primary); - Class targetClass = (Class) target.getClass(); - return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) - .create(target); + public boolean match(R actualResource, R desiredResource, Context context) { + return matcher.match(actualResource, desiredResource, context); + } + + public void delete(P primary, Context context) { + if (!addOwnerReference) { + var resource = getResource(primary); + resource.ifPresent(r -> client.resource(r).delete()); + } } @SuppressWarnings("unchecked") - @Override - protected R update(R actual, R target, P primary, Context context) { - log.debug("Updating target resource with type: {}, with id: {}", target.getClass(), - ResourceID.fromResource(target)); - Class targetClass = (Class) target.getClass(); - var updatedActual = resourceUpdatePreProcessor.replaceSpecOnActual(actual, target); - return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace()) - .replace(updatedActual); + protected NonNamespaceOperation, Resource> prepare(R desired, + P primary, String actionName) { + log.debug("{} target resource with type: {}, with id: {}", + actionName, + desired.getClass(), + ResourceID.fromResource(desired)); + if (addOwnerReference) { + desired.addOwnerReference(primary); + } + Class targetClass = (Class) desired.getClass(); + return client.resources(targetClass).inNamespace(desired.getMetadata().getNamespace()); } @Override public EventSource eventSource(EventSourceContext

context) { - initResourceMatcherAndUpdatePreProcessorIfNotSet(context.getConfigurationService()); if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); @@ -129,14 +143,6 @@ public KubernetesDependentResource setInformerEventSource( return this; } - @Override - public void delete(P primary, Context context) { - if (!addOwnerReference) { - var resource = getResource(primary); - resource.ifPresent(r -> client.resource(r).delete()); - } - } - @SuppressWarnings("unchecked") protected Class resourceType() { return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); @@ -151,31 +157,4 @@ public Optional getResource(P primaryResource) { public void setKubernetesClient(KubernetesClient kubernetesClient) { this.client = kubernetesClient; } - - /** - * Override this method to configure resource matcher - * - * @param configurationService config service to mainly access object mapper - */ - protected void initResourceMatcherAndUpdatePreProcessorIfNotSet( - ConfigurationService configurationService) { - if (resourceMatcher == null) { - resourceMatcher = new DesiredValueMatcher(configurationService.getObjectMapper()); - } - if (resourceUpdatePreProcessor == null) { - resourceUpdatePreProcessor = - new ResourceUpdatePreProcessor<>(configurationService.getResourceCloner()); - } - } - - public KubernetesDependentResource setResourceMatcher(ResourceMatcher resourceMatcher) { - this.resourceMatcher = resourceMatcher; - return this; - } - - public KubernetesDependentResource setResourceUpdatePreProcessor( - ResourceUpdatePreProcessor resourceUpdatePreProcessor) { - this.resourceUpdatePreProcessor = resourceUpdatePreProcessor; - return this; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java deleted file mode 100644 index 0d86709ca0..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceMatcher.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.Context; - -public interface ResourceMatcher { - - boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java deleted file mode 100644 index c7a16381a9..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.Secret; -import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.Cloner; - -public class ResourceUpdatePreProcessor { - - private final Cloner cloner; - - public ResourceUpdatePreProcessor(Cloner cloner) { - this.cloner = cloner; - } - - public R replaceSpecOnActual(R actual, R desired) { - var clonedActual = cloner.clone(actual); - if (desired instanceof ConfigMap) { - ((ConfigMap) clonedActual).setData(((ConfigMap) desired).getData()); - ((ConfigMap) clonedActual).setBinaryData((((ConfigMap) desired).getBinaryData())); - return clonedActual; - } else if (desired instanceof Secret) { - ((Secret) clonedActual).setData(((Secret) desired).getData()); - ((Secret) clonedActual).setStringData(((Secret) desired).getStringData()); - return clonedActual; - } else { - var desiredSpec = ReconcilerUtils.getSpec(desired); - ReconcilerUtils.setSpec(clonedActual, desiredSpec); - return clonedActual; - } - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index b29209f9e5..a706e244ab 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -77,16 +77,6 @@ private void propagateEvent(T object) { }); } - @Override - public void start() { - manager().start(); - } - - @Override - public void stop() { - manager().stop(); - } - /** * Retrieves the informed resource associated with the specified primary resource as defined by * the function provided when this InformerEventSource was created diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java similarity index 53% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index d3e013d54d..6302c20e6d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/DesiredValueMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -4,47 +4,55 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.Context; import com.fasterxml.jackson.databind.ObjectMapper; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -class DesiredValueMatcherTest { +class GenericKubernetesResourceMatcherTest { - DesiredValueMatcher desiredValueMatcher = new DesiredValueMatcher(new ObjectMapper()); + private static final Context context = mock(Context.class); + static { + final var configurationService = mock(ConfigurationService.class); + when(configurationService.getObjectMapper()).thenReturn(new ObjectMapper()); + when(context.getConfigurationService()).thenReturn(configurationService); + } @Test void checksIfDesiredValuesAreTheSame() { var target1 = createDeployment(); var desired1 = createDeployment(); - assertThat(desiredValueMatcher.match(target1, desired1, null)).isTrue(); + final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class); + assertThat(matcher.match(target1, desired1, context)).isTrue(); var target2 = createDeployment(); var desired2 = createDeployment(); target2.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); - assertThat(desiredValueMatcher.match(target2, desired2, null)) + assertThat(matcher.match(target2, desired2, context)) .withFailMessage("Additive changes should be ok") .isTrue(); var target3 = createDeployment(); var desired3 = createDeployment(); desired3.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); - assertThat(desiredValueMatcher.match(target3, desired3, null)) + assertThat(matcher.match(target3, desired3, context)) .withFailMessage("Removed value should not be ok") .isFalse(); var target4 = createDeployment(); var desired4 = createDeployment(); target4.getSpec().setReplicas(2); - assertThat(desiredValueMatcher.match(target4, desired4, null)) + assertThat(matcher.match(target4, desired4, context)) .withFailMessage("Changed values are not ok") .isFalse(); } Deployment createDeployment() { - Deployment deployment = - ReconcilerUtils.loadYaml( - Deployment.class, DesiredValueMatcherTest.class, "nginx-deployment.yaml"); - return deployment; + return ReconcilerUtils.loadYaml( + Deployment.class, GenericKubernetesResourceMatcherTest.class, "nginx-deployment.yaml"); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java similarity index 51% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java index a4907f13b7..941738ad7f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java @@ -7,13 +7,26 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -class ResourceUpdatePreProcessorTest { +class GenericResourceUpdatePreProcessorTest { + + private static final Context context = mock(Context.class); + + static { + final var configurationService = mock(ConfigurationService.class); + when(configurationService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); + when(context.getConfigurationService()).thenReturn(configurationService); + } + + ResourceUpdatePreProcessor processor = + GenericResourceUpdatePreProcessor.processorFor(Deployment.class); - ResourceUpdatePreProcessor resourceUpdatePreProcessor = - new ResourceUpdatePreProcessor<>(ConfigurationService.DEFAULT_CLONER); @Test void preservesValues() { @@ -24,7 +37,7 @@ void preservesValues() { actual.getMetadata().setResourceVersion("1234"); actual.getSpec().setRevisionHistoryLimit(5); - var result = resourceUpdatePreProcessor.replaceSpecOnActual(actual, desired); + var result = processor.replaceSpecOnActual(actual, desired, context); assertThat(result.getMetadata().getLabels().get("additionalActualKey")).isEqualTo("value"); assertThat(result.getMetadata().getResourceVersion()).isEqualTo("1234"); @@ -32,10 +45,8 @@ void preservesValues() { } Deployment createDeployment() { - Deployment deployment = - ReconcilerUtils.loadYaml( - Deployment.class, ResourceUpdatePreProcessorTest.class, "nginx-deployment.yaml"); - return deployment; + return ReconcilerUtils.loadYaml( + Deployment.class, GenericResourceUpdatePreProcessorTest.class, "nginx-deployment.yaml"); } } 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 index dd1e005fc1..67a8937ac4 100644 --- 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 @@ -3,7 +3,6 @@ import java.time.Duration; import java.util.*; import java.util.function.Function; -import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; @@ -27,6 +26,7 @@ public class AnnotationControllerConfiguration private final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; + private List specs; public AnnotationControllerConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -142,42 +142,42 @@ public static T valueOrDefault( } } + @SuppressWarnings({"rawtypes", "unchecked"}) @Override public List getDependentResources() { - final var dependents = - valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); - if (dependents.length == 0) { - return Collections.emptyList(); - } + if (specs == null) { + final var dependents = + valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); + if (dependents.length == 0) { + return Collections.emptyList(); + } - List resourceSpecs = new ArrayList<>(dependents.length); - for (Dependent dependent : dependents) { - - final Class dependentType = dependent.type(); - if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { - final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); - final var namespaces = - Utils.valueOrDefault( - kubeDependent, - KubernetesDependent::namespaces, - this.getNamespaces().toArray(new String[0])); - final var labelSelector = - Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); - final var addOwnerReference = - Utils.valueOrDefault( - kubeDependent, - KubernetesDependent::addOwnerReference, - KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); - KubernetesDependentResourceConfig config = - new KubernetesDependentResourceConfig( - addOwnerReference, namespaces, labelSelector, getConfigurationService()); - resourceSpecs.add(new DependentResourceSpec(dependentType, config)); - } else { - resourceSpecs.add(new DependentResourceSpec(dependentType)); + specs = new ArrayList<>(dependents.length); + for (Dependent dependent : dependents) { + final Class dependentType = dependent.type(); + if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { + final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); + final var namespaces = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::namespaces, + this.getNamespaces().toArray(new String[0])); + final var labelSelector = + Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); + final var addOwnerReference = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::addOwnerReference, + KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + KubernetesDependentResourceConfig config = + new KubernetesDependentResourceConfig( + addOwnerReference, namespaces, labelSelector, getConfigurationService()); + specs.add(new DependentResourceSpec(dependentType, config)); + } else { + specs.add(new DependentResourceSpec(dependentType)); + } } } - return Arrays.stream(dependents) - .map(d -> new DependentResourceSpec(d.type())) - .collect(Collectors.toList()); + return specs; } } 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/runtime/AnnotationControllerConfigurationTest.java new file mode 100644 index 0000000000..b9556aba1c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java @@ -0,0 +1,58 @@ +package io.javaoperatorsdk.operator.config.runtime; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +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.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.sample.readonly.ReadOnlyDependent; + +import static org.junit.jupiter.api.Assertions.*; + +class AnnotationControllerConfigurationTest { + + @Test + void getDependentResources() { + var configuration = new AnnotationControllerConfiguration<>(new NoDepReconciler()); + var dependents = configuration.getDependentResources(); + assertTrue(dependents.isEmpty()); + + configuration = new AnnotationControllerConfiguration<>(new OneDepReconciler()); + dependents = configuration.getDependentResources(); + assertFalse(dependents.isEmpty()); + assertEquals(1, dependents.size()); + final var dependentSpec = dependents.get(0); + assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass()); + final var maybeConfig = dependentSpec.getDependentResourceConfiguration(); + assertTrue(maybeConfig.isPresent()); + assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig); + final var config = (KubernetesDependentResourceConfig) maybeConfig.orElseThrow(); + assertEquals(1, config.namespaces().length); + assertEquals(OneDepReconciler.CONFIGURED_NS, config.namespaces()[0]); + } + + + @ControllerConfiguration(namespaces = OneDepReconciler.CONFIGURED_NS, + dependents = @Dependent(type = ReadOnlyDependent.class)) + private static class OneDepReconciler implements Reconciler { + + public static final String CONFIGURED_NS = "foo"; + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) { + return null; + } + } + + private static class NoDepReconciler implements Reconciler { + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) { + return null; + } + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ConfigMapReader.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ConfigMapReader.java new file mode 100644 index 0000000000..ffff1bf78a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ConfigMapReader.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.sample.readonly; + +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.Version; + +@Version("v1") +@Group("josdk.io") +public class ConfigMapReader extends CustomResource implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java new file mode 100644 index 0000000000..0285d47b83 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.readonly; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; + +public class ReadOnlyDependent extends KubernetesDependentResource { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index d529f19c3d..21f02c6c3a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -20,29 +21,29 @@ public class StandaloneDependentTestReconciler private KubernetesClient kubernetesClient; - KubernetesDependentResource configMapDependent; + KubernetesDependentResource deploymentDependent; public StandaloneDependentTestReconciler() { - configMapDependent = new DeploymentDependentResource(); + deploymentDependent = new DeploymentDependentResource(); } @Override public List prepareEventSources( EventSourceContext context) { - return List.of(configMapDependent.eventSource(context)); + return List.of(deploymentDependent.eventSource(context)); } @Override public UpdateControl reconcile( StandaloneDependentTestCustomResource resource, Context context) { - configMapDependent.reconcile(resource, context); + deploymentDependent.reconcile(resource, context); return UpdateControl.noUpdate(); } @Override public void setKubernetesClient(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; - configMapDependent.setKubernetesClient(kubernetesClient); + deploymentDependent.setKubernetesClient(kubernetesClient); } @Override @@ -50,8 +51,9 @@ public KubernetesClient getKubernetesClient() { return this.kubernetesClient; } - private class DeploymentDependentResource extends - KubernetesDependentResource { + private static class DeploymentDependentResource extends + KubernetesDependentResource + implements Creator { @Override protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java index 67c553268f..b38d3cfdf8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java @@ -8,6 +8,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -18,9 +20,11 @@ import static java.lang.String.format; public class SchemaDependentResource - extends AbstractDependentResource + extends AbstractDependentResource implements EventSourceProvider, - DependentResourceConfigurator { + DependentResourceConfigurator, + Creator, + Deleter { private MySQLDbConfig dbConfig; private int pollPeriod = 500; @@ -47,12 +51,7 @@ public Schema desired(MySQLSchema primary, Context context) { } @Override - protected boolean match(Schema actual, Schema target, Context context) { - return actual.equals(target); - } - - @Override - protected Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { + public void create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { final var schema = SchemaService.createSchemaAndRelatedUser( connection, @@ -63,18 +62,12 @@ protected Schema create(Schema target, MySQLSchema mySQLSchema, Context context) // put the newly built schema in the context to let the reconciler know we just built it context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); - return schema; } catch (SQLException e) { MySQLSchemaReconciler.log.error("Error while creating Schema", e); throw new IllegalStateException(e); } } - @Override - protected Schema update(Schema actual, Schema target, MySQLSchema mySQLSchema, Context context) { - throw new IllegalStateException("Target schema should not be changed: " + mySQLSchema); - } - private Connection getConnection() throws SQLException { String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort()); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java index 884cc6d2c9..5dfa20a571 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -12,20 +13,12 @@ import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*; public class SecretDependentResource extends KubernetesDependentResource - implements AssociatedSecondaryResourceIdentifier { + implements AssociatedSecondaryResourceIdentifier, Updater { private static String encode(String value) { return Base64.getEncoder().encodeToString(value.getBytes()); } - // An alternative would be to override reconcile() method and exclude the update part. - @Override - protected Secret update(Secret actual, Secret target, MySQLSchema primary, Context context) { - throw new IllegalStateException( - "Secret should not be updated. Secret: " + target + " for custom resource: " - + primary); - } - @Override protected Secret desired(MySQLSchema schema, Context context) { return new SecretBuilder() @@ -41,7 +34,7 @@ protected Secret desired(MySQLSchema schema, Context context) { } @Override - protected boolean match(Secret actual, Secret target, Context context) { + public boolean match(Secret actual, Secret target, Context context) { return ResourceID.fromResource(actual).equals(ResourceID.fromResource(target)); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 3a68973165..56b80b6c82 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -5,11 +5,14 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; @KubernetesDependent(labelSelector = "app.kubernetes.io/managed-by=tomcat-operator") -public class DeploymentDependentResource extends KubernetesDependentResource { +public class DeploymentDependentResource extends KubernetesDependentResource + implements Creator, Updater { private static String tomcatImage(Tomcat tomcat) { return "tomcat:" + tomcat.getSpec().getVersion(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index a77d13b345..66ddebf31e 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -5,9 +5,12 @@ import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -public class ServiceDependentResource extends KubernetesDependentResource { +public class ServiceDependentResource extends KubernetesDependentResource + implements Creator, Updater { @Override protected Service desired(Tomcat tomcat, Context context) { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index b3200903d1..d7fd52f788 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -152,7 +153,7 @@ private static String serviceName(WebPage nginx) { private class ConfigMapDependentResource extends KubernetesDependentResource implements - AssociatedSecondaryResourceIdentifier { + AssociatedSecondaryResourceIdentifier, Updater { @Override protected ConfigMap desired(WebPage webPage, Context context) { @@ -169,15 +170,14 @@ protected ConfigMap desired(WebPage webPage, Context context) { } @Override - protected boolean match(ConfigMap actual, ConfigMap target, Context context) { + public boolean match(ConfigMap actual, ConfigMap target, Context context) { return StringUtils.equals( actual.getData().get("index.html"), target.getData().get("index.html")); } @Override - protected ConfigMap update( - ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - var cm = super.update(actual, target, primary, context); + public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + super.update(actual, target, primary, context); var ns = actual.getMetadata().getNamespace(); log.info("Restarting pods because HTML has changed in {}", ns); kubernetesClient @@ -185,7 +185,6 @@ protected ConfigMap update( .inNamespace(ns) .withLabel("app", deploymentName(primary)) .delete(); - return cm; } @Override From c5966219e0ef8e2ccd14dbddca227c769b163447 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Feb 2022 11:08:24 +0100 Subject: [PATCH 0317/1608] chore(deps): bump spring-boot.version from 2.6.3 to 2.6.4 (#972) Bumps `spring-boot.version` from 2.6.3 to 2.6.4. Updates `spring-boot-dependencies` from 2.6.3 to 2.6.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.3...v2.6.4) Updates `spring-boot-maven-plugin` from 2.6.3 to 2.6.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-dependencies dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 420bf65c12..a8d47595aa 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ 1.13.0 3.22.0 4.1.1 - 2.6.3 + 2.6.4 1.8.3 2.11 From 844323f8daac61d966444e9c3083ea2f152b55b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 25 Feb 2022 11:08:53 +0100 Subject: [PATCH 0318/1608] Temporal resource cache in Event Source (#965) --- .../informer/InformerConfiguration.java | 1 + .../operator/processing/Controller.java | 2 +- .../kubernetes/KubernetesDependent.java | 1 - .../KubernetesDependentResource.java | 30 ++- .../processing/event/EventProcessor.java | 50 +--- .../ControllerResourceEventSource.java | 32 +-- .../event/source/informer/EventRecorder.java | 71 ++++++ .../source/informer/InformerEventSource.java | 220 ++++++++++++++---- .../source/informer/InformerManager.java | 6 +- .../source/informer/InformerWrapper.java | 1 + .../informer/ManagedInformerEventSource.java | 82 ++++++- .../informer/TemporaryResourceCache.java | 110 +++++++++ .../KubernetesDependentResourceTest.java | 77 ++++++ .../processing/event/EventProcessorTest.java | 103 +++----- .../ControllerResourceEventSourceTest.java | 22 -- .../source/informer/EventRecorderTest.java | 80 +++++++ .../informer/InformerEventSourceTest.java | 194 +++++++++++++++ .../informer/TemporaryResourceCacheTest.java | 100 ++++++++ .../CreateUpdateDependentEventFilterIT.java | 88 +++++++ .../StandaloneDependentResourceIT.java | 36 ++- ...teUpdateEventFilterTestCustomResource.java | 22 ++ ...dateEventFilterTestCustomResourceSpec.java | 16 ++ ...teEventFilterTestCustomResourceStatus.java | 7 + ...CreateUpdateEventFilterTestReconciler.java | 120 ++++++++++ ...StandaloneDependentTestCustomResource.java | 3 +- ...daloneDependentTestCustomResourceSpec.java | 23 ++ .../StandaloneDependentTestReconciler.java | 35 ++- .../operator/sample/WebPageReconciler.java | 9 - 28 files changed, 1304 insertions(+), 237 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java rename operator-framework/src/test/java/io/javaoperatorsdk/operator/{dependent => }/StandaloneDependentResourceIT.java (56%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceSpec.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index acb5bd23c4..0d08b65f99 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 @@ -37,6 +37,7 @@ protected DefaultInformerConfiguration(ConfigurationService service, String labe Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); } + public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { return secondaryToPrimaryResourcesIdSet; } 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 89aabe0d9b..e8996c351c 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 @@ -214,7 +214,7 @@ public void start() throws OperatorException { } final var context = new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), configurationService(), kubernetesClient); prepareEventSources(context).forEach(eventSourceManager::registerEventSource); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java index c0922f569f..45d2095d1a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependent.java @@ -32,5 +32,4 @@ * @return the label selector */ String labelSelector() default EMPTY_STRING; - } 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 a0513b1234..8ce3560fdc 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 @@ -75,17 +75,16 @@ private void configureWith(ConfigurationService configService, String labelSelec .withPrimaryResourcesRetriever(primaryResourcesRetriever) .withAssociatedSecondaryResourceIdentifier(secondaryResourceIdentifier) .build(); - configureWith(configService, new InformerEventSource<>(ic, client), addOwnerReference); + configureWith(new InformerEventSource<>(ic, client), addOwnerReference); } /** * Use to share informers between event more resources. - * - * @param configurationService get configs + * * @param informerEventSource informer to use * @param addOwnerReference to the created resource */ - public void configureWith(ConfigurationService configurationService, + public void configureWith( InformerEventSource informerEventSource, boolean addOwnerReference) { this.informerEventSource = informerEventSource; @@ -93,12 +92,29 @@ public void configureWith(ConfigurationService configurationService, } public void create(R target, P primary, Context context) { - prepare(target, primary, "Creating").create(target); + var resourceID = ResourceID.fromResource(target); + try { + informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID); + var created = prepare(target, primary, "Creating").create(target); + informerEventSource.handleRecentResourceCreate(created); + } catch (RuntimeException e) { + informerEventSource.cleanupOnCreateOrUpdateEventFiltering(resourceID); + throw e; + } } public void update(R actual, R target, P primary, Context context) { - var updatedActual = processor.replaceSpecOnActual(actual, target, context); - prepare(target, primary, "Updating").replace(updatedActual); + var resourceID = ResourceID.fromResource(target); + try { + var updatedActual = processor.replaceSpecOnActual(actual, target, context); + informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID); + var updated = prepare(target, primary, "Updating").replace(updatedActual); + informerEventSource.handleRecentResourceUpdate(updated, + actual.getMetadata().getResourceVersion()); + } catch (RuntimeException e) { + informerEventSource.cleanupOnCreateOrUpdateEventFiltering(resourceID); + throw e; + } } public boolean match(R actualResource, R desiredResource, Context context) { 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 29f9adab55..81e637a2f6 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 @@ -29,7 +29,6 @@ import io.javaoperatorsdk.operator.processing.retry.RetryExecution; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; class EventProcessor implements EventHandler, LifecycleAware { @@ -50,7 +49,7 @@ class EventProcessor implements EventHandler, LifecycleAw EventProcessor(EventSourceManager eventSourceManager) { this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), ExecutorServiceManager.instance().executorService(), eventSourceManager.getController().getConfiguration().getName(), new ReconciliationDispatcher<>(eventSourceManager.getController()), @@ -73,7 +72,7 @@ class EventProcessor implements EventHandler, LifecycleAw Retry retry, Metrics metrics) { this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), null, relatedControllerName, reconciliationDispatcher, @@ -208,12 +207,12 @@ void eventProcessingFinished( if (eventMarker.deleteEventPresent(resourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { + postExecutionControl.getUpdatedCustomResource().ifPresent(r -> { + eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate(r, + executionScope.getResource().getMetadata().getResourceVersion()); + }); if (eventMarker.eventPresent(resourceID)) { - if (isCacheReadyForInstantReconciliation(executionScope, postExecutionControl)) { - submitReconciliationExecution(resourceID); - } else { - postponeReconciliationAndHandleCacheSyncEvent(resourceID); - } + submitReconciliationExecution(resourceID); } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); } @@ -223,41 +222,6 @@ void eventProcessingFinished( } } - private void postponeReconciliationAndHandleCacheSyncEvent(ResourceID resourceID) { - eventSourceManager.getControllerResourceEventSource().whitelistNextEvent(resourceID); - } - - private boolean isCacheReadyForInstantReconciliation( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { - if (!postExecutionControl.customResourceUpdatedDuringExecution()) { - return true; - } - String originalResourceVersion = getVersion(executionScope.getResource()); - String customResourceVersionAfterExecution = - getVersion( - postExecutionControl - .getUpdatedCustomResource() - .orElseThrow( - () -> new IllegalStateException( - "Updated custom resource must be present at this point of time"))); - String cachedCustomResourceVersion = - getVersion( - cache - .get(executionScope.getCustomResourceID()) - .orElseThrow( - () -> new IllegalStateException( - "Cached custom resource must be present at this point"))); - - if (cachedCustomResourceVersion.equals(customResourceVersionAfterExecution)) { - return true; - } - // If the cached resource version equals neither the version before nor after execution - // probably an update happened on the custom resource independent of the framework during - // reconciliation. We cannot tell at this point if it happened before our update or before. - // (Well we could if we would parse resource version, but that should not be done by definition) - return !cachedCustomResourceVersion.equals(originalResourceVersion); - } - private void reScheduleExecutionIfInstructed( PostExecutionControl postExecutionControl, R customResource) { postExecutionControl diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 2a224a87df..801eae09ee 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -13,7 +13,6 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; @@ -25,30 +24,19 @@ public class ControllerResourceEventSource implements ResourceEventHandler { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; - private static final Logger log = LoggerFactory.getLogger(ControllerResourceEventSource.class); private final Controller controller; private final ResourceEventFilter filter; - private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; public ControllerResourceEventSource(Controller controller) { super(controller.getCRClient(), controller.getConfiguration()); this.controller = controller; - var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), ResourceEventFilters.markedForDeletion(), ResourceEventFilters.generationAware(), - null }; - - if (controller.getConfiguration().isGenerationAware()) { - onceWhitelistEventFilterEventFilter = new OnceWhitelistEventFilterEventFilter<>(); - filters[filters.length - 1] = onceWhitelistEventFilterEventFilter; - } else { - onceWhitelistEventFilterEventFilter = null; - } if (controller.getConfiguration().getEventFilter() != null) { filter = controller.getConfiguration().getEventFilter().and(ResourceEventFilters.or(filters)); } else { @@ -87,36 +75,22 @@ public void eventReceived(ResourceAction action, T resource, T oldResource) { @Override public void onAdd(T resource) { + super.onAdd(resource); eventReceived(ResourceAction.ADDED, resource, null); } @Override public void onUpdate(T oldCustomResource, T newCustomResource) { + super.onUpdate(oldCustomResource, newCustomResource); eventReceived(ResourceAction.UPDATED, newCustomResource, oldCustomResource); } @Override public void onDelete(T resource, boolean b) { + super.onDelete(resource, b); eventReceived(ResourceAction.DELETED, resource, null); } - public ResourceCache getResourceCache() { - return manager(); - } - - /** - * This will ensure that the next event received after this method is called will not be filtered - * out. - * - * @param resourceID - to which the event is related - */ - public void whitelistNextEvent(ResourceID resourceID) { - if (onceWhitelistEventFilterEventFilter != null) { - onceWhitelistEventFilterEventFilter.whitelistNextEvent(resourceID); - } - } - - private void handleKubernetesClientException(Exception e) { KubernetesClientException ke = (KubernetesClientException) e; if (404 == ke.getCode()) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java new file mode 100644 index 0000000000..284b749f07 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java @@ -0,0 +1,71 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class EventRecorder { + + private final Map> resourceEvents = new ConcurrentHashMap<>(); + + void startEventRecording(ResourceID resourceID) { + resourceEvents.putIfAbsent(resourceID, new ArrayList<>(5)); + } + + public boolean isRecordingFor(ResourceID resourceID) { + return resourceEvents.get(resourceID) != null; + } + + public void stopEventRecording(ResourceID resourceID) { + resourceEvents.remove(resourceID); + } + + public void recordEvent(R resource) { + resourceEvents.get(ResourceID.fromResource(resource)).add(resource); + } + + public boolean containsEventWithResourceVersion(ResourceID resourceID, String resourceVersion) { + List events = resourceEvents.get(resourceID); + if (events == null) { + return false; + } + if (events.isEmpty()) { + return false; + } else { + return events.stream() + .anyMatch(e -> e.getMetadata().getResourceVersion().equals(resourceVersion)); + } + } + + public boolean containsEventWithVersionButItsNotLastOne( + ResourceID resourceID, String resourceVersion) { + List resources = resourceEvents.get(resourceID); + if (resources == null) { + throw new IllegalStateException( + "Null events list, this is probably a result of invalid usage of the " + + "InformerEventSource. Resource ID: " + resourceID); + } + if (resources.isEmpty()) { + throw new IllegalStateException("No events for resource id: " + resourceID); + } + return !resources + .get(resources.size() - 1) + .getMetadata() + .getResourceVersion() + .equals(resourceVersion); + } + + public R getLastEvent(ResourceID resourceID) { + List resources = resourceEvents.get(resourceID); + if (resources == null) { + throw new IllegalStateException( + "Null events list, this is probably a result of invalid usage of the " + + "InformerEventSource. Resource ID: " + resourceID); + } + return resources.get(resources.size() - 1); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index a706e244ab..2f8705b63a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -1,8 +1,9 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; @@ -11,87 +12,224 @@ import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; -public class InformerEventSource - extends ManagedInformerEventSource> - implements ResourceCache, ResourceEventHandler { +/** + *

+ * Wraps informer(s) so it is connected to the eventing system of the framework. Note that since + * it's it is built on top of Informers, it also support caching resources using caching from + * fabric8 client Informer caches and additional caches described below. + *

+ *

+ * InformerEventSource also supports two features to better handle events and caching of resources + * on top of Informers from fabric8 Kubernetes client. These two features implementation wise are + * related to each other: + *

+ *
+ *

+ * 1. API that allows to make sure the cache contains the fresh resource after an update. This is + * important for {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} and + * mainly for + * {@link io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource} + * so after reconcile if getResource() called always return the fresh resource. To achieve this + * handleRecentResourceUpdate() and handleRecentResourceCreate() needs to be called explicitly after + * resource created/updated using the kubernetes client. (These calls are done automatically by + * KubernetesDependentResource implementation.). In the background this will store the new resource + * in a temporary cache {@link TemporaryResourceCache} which do additional checks. After a new event + * is received the cachec object is removed from this cache, since in general then it is already in + * the cache of informer. + *

+ *
+ *

+ * 2. Additional API is provided that is ment to be used with the combination of the previous one, + * and the goal is to filter out events that are the results of updates and creates made by the + * controller itself. For example if in reconciler a ConfigMaps is created, there should be an + * Informer in place to handle change events of that ConfigMap, but since it has bean created (or + * updated) by the reconciler this should not trigger an additional reconciliation by default. In + * order to achieve this prepareForCreateOrUpdateEventFiltering(..) method needs to be called before + * the operation of the k8s client. And the operation from point 1. after the k8s client call. See + * it's usage in CreateUpdateEventFilterTestReconciler integration test for the usage. (Again this + * is managed for the developer if using dependent resources.)
+ * Roughly it works in a way that before the K8S API call is made, we set mark the resource ID, and + * from that point informer won't propagate events further just will start record them. After the + * client operation is done, it's checked and analysed what events were received and based on that + * it will propagate event or not and/or put the new resource into the temporal cache - so if the + * event not arrived yet about the update will be able to filter it in the future. + *

+ * + * @param resource type watching + * @param

type of the primary resource + */ +public class InformerEventSource + extends ManagedInformerEventSource> + implements ResourceCache, ResourceEventHandler { + + private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); - private final InformerConfiguration configuration; + private final InformerConfiguration configuration; + private final EventRecorder eventRecorder = new EventRecorder<>(); - public InformerEventSource(InformerConfiguration configuration, - EventSourceContext

context) { + public InformerEventSource( + InformerConfiguration configuration, EventSourceContext

context) { super(context.getClient().resources(configuration.getResourceClass()), configuration); this.configuration = configuration; } - public InformerEventSource(InformerConfiguration configuration, - KubernetesClient client) { + public InformerEventSource(InformerConfiguration configuration, KubernetesClient client) { super(client.resources(configuration.getResourceClass()), configuration); this.configuration = configuration; } @Override - public void onAdd(T t) { - propagateEvent(t); + public void onAdd(R resource) { + onAddOrUpdate("add", resource, () -> InformerEventSource.super.onAdd(resource)); } @Override - public void onUpdate(T oldObject, T newObject) { - if (newObject == null) { - // this is a fix for this potential issue with informer: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 - propagateEvent(oldObject); - return; - } + public void onUpdate(R oldObject, R newObject) { + onAddOrUpdate("update", newObject, + () -> InformerEventSource.super.onUpdate(oldObject, newObject)); + } - if (oldObject.getMetadata().getResourceVersion() - .equals(newObject.getMetadata().getResourceVersion())) { + private synchronized void onAddOrUpdate(String operation, R newObject, Runnable superOnOp) { + var resourceID = ResourceID.fromResource(newObject); + if (eventRecorder.isRecordingFor(resourceID)) { + log.info("Recording event for: " + resourceID); + eventRecorder.recordEvent(newObject); return; } - propagateEvent(newObject); + if (temporalCacheHasResourceWithVersionAs(newObject)) { + log.debug( + "Skipping event propagation for {}, since was a result of a reconcile action. Resource ID: {}", + operation, + ResourceID.fromResource(newObject)); + superOnOp.run(); + } else { + superOnOp.run(); + log.debug( + "Propagating event for {}, resource with same version not result of a reconciliation. Resource ID: {}", + operation, + resourceID); + propagateEvent(newObject); + } } @Override - public void onDelete(T t, boolean b) { - propagateEvent(t); + public void onDelete(R r, boolean b) { + super.onDelete(r, b); + propagateEvent(r); } - private void propagateEvent(T object) { + private void propagateEvent(R object) { var primaryResourceIdSet = configuration.getPrimaryResourcesRetriever().associatedPrimaryResources(object); if (primaryResourceIdSet.isEmpty()) { return; } - primaryResourceIdSet.forEach(resourceId -> { - Event event = new Event(resourceId); - /* - * In fabric8 client for certain cases informers can be created on in a way that they are - * automatically started, what would cause a NullPointerException here, since an event might - * be received between creation and registration. - */ - final EventHandler eventHandler = getEventHandler(); - if (eventHandler != null) { - eventHandler.handleEvent(event); - } - }); + primaryResourceIdSet.forEach( + resourceId -> { + Event event = new Event(resourceId); + /* + * In fabric8 client for certain cases informers can be created on in a way that they are + * automatically started, what would cause a NullPointerException here, since an event + * might be received between creation and registration. + */ + final EventHandler eventHandler = getEventHandler(); + if (eventHandler != null) { + eventHandler.handleEvent(event); + } + }); } /** * Retrieves the informed resource associated with the specified primary resource as defined by * the function provided when this InformerEventSource was created - * + * * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ @Override - public Optional getAssociated(P resource) { + public Optional getAssociated(P resource) { final var id = configuration.getAssociatedResourceIdentifier().associatedSecondaryID(resource); return get(id); } + public InformerConfiguration getConfiguration() { + return configuration; + } + @Override - public Stream list(String namespace, Predicate predicate) { - return manager().list(namespace, predicate); + public void handleRecentResourceUpdate(R resource, String previousResourceVersion) { + handleRecentCreateOrUpdate(resource, + () -> super.handleRecentResourceUpdate(resource, previousResourceVersion)); + } + + @Override + public void handleRecentResourceCreate(R resource) { + handleRecentCreateOrUpdate(resource, () -> super.handleRecentResourceCreate(resource)); + } + + private synchronized void handleRecentCreateOrUpdate(R resource, Runnable runnable) { + if (eventRecorder.isRecordingFor(ResourceID.fromResource(resource))) { + handleRecentResourceOperationAndStopEventRecording(resource); + } else { + runnable.run(); + } + } + + /** + * There can be the following cases: + *

    + *
  • 1. Did not receive the event yet for the target resource, then we need to put it to temp + * cache. Because event will arrive. Note that this not necessary mean that the even is not sent + * yet (we are in sync context). Also does not mean that there are no more events received after + * that. But during the event processing (onAdd, onUpdate) we make sure that the propagation just + * skipped for the right event.
  • + *
  • 2. Received the event about the operation already, it was the last. This means already is + * on cache of informer. So we have to do nothing. Since it was just recorded and not propagated. + *
  • + *
  • 3. Received the event but more events received since, so those were not propagated yet. So + * an event needs to be propagated to compensate.
  • + *
+ * + * @param resource just created or updated resource + */ + private synchronized void handleRecentResourceOperationAndStopEventRecording(R resource) { + ResourceID resourceID = ResourceID.fromResource(resource); + try { + if (!eventRecorder.containsEventWithResourceVersion( + resourceID, resource.getMetadata().getResourceVersion())) { + log.debug( + "Did not found event in buffer with target version and resource id: {}", resourceID); + temporaryResourceCache.unconditionallyCacheResource(resource); + } else if (eventRecorder.containsEventWithVersionButItsNotLastOne( + resourceID, resource.getMetadata().getResourceVersion())) { + R lastEvent = eventRecorder.getLastEvent(resourceID); + log.debug( + "Found events in event buffer but the target event is not last for id: {}. Propagating event.", + resourceID); + propagateEvent(lastEvent); + } + } finally { + eventRecorder.stopEventRecording(resourceID); + } } + + public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID) { + log.info("Starting event recording for: {}", resourceID); + eventRecorder.startEventRecording(resourceID); + } + + /** + * Mean to be called to clean up in case of an exception from the client. Usually in a catch + * block. + * + * @param resourceID of the resource + */ + public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID) { + log.info("Stopping event recording for: {}", resourceID); + eventRecorder.stopEventRecording(resourceID); + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index e65f34df48..33f962e7ed 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -139,6 +139,10 @@ public T remove(ResourceID key) { @Override public void put(ResourceID key, T resource) { getSource(key.getNamespace().orElse(ANY_NAMESPACE_MAP_KEY)) - .ifPresent(c -> c.put(key, resource)); + .ifPresentOrElse(c -> c.put(key, resource), + () -> log.warn( + "Cannot put resource in the cache. No related cache found: {}. Resource: {}", + key, resource)); } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index c7de41f331..710f230580 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -81,4 +81,5 @@ public T remove(ResourceID key) { public void put(ResourceID key, T resource) { cache.put(key, resource); } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 1dcfa2aa0e..47de996914 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 @@ -1,17 +1,30 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; public abstract class ManagedInformerEventSource> extends CachingEventSource - implements ResourceEventHandler { + implements ResourceEventHandler, ResourceCache { + + private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); + + protected TemporaryResourceCache temporaryResourceCache = new TemporaryResourceCache<>(this); protected ManagedInformerEventSource( MixedOperation, Resource> client, C configuration) { @@ -19,6 +32,21 @@ protected ManagedInformerEventSource( manager().initSources(client, configuration, this); } + @Override + public void onAdd(R resource) { + temporaryResourceCache.removeResourceFromCache(resource); + } + + @Override + public void onUpdate(R oldObj, R newObj) { + temporaryResourceCache.removeResourceFromCache(newObj); + } + + @Override + public void onDelete(R obj, boolean deletedFinalStateUnknown) { + temporaryResourceCache.removeResourceFromCache(obj); + } + @Override protected UpdatableCache initCache() { return new InformerManager<>(); @@ -39,4 +67,56 @@ public void stop() { super.stop(); manager().stop(); } + + public void handleRecentResourceUpdate(R resource, String previousResourceVersion) { + temporaryResourceCache.putUpdatedResource(resource, previousResourceVersion); + } + + public void handleRecentResourceCreate(R resource) { + temporaryResourceCache.putAddedResource(resource); + } + + @Override + public Optional get(ResourceID resourceID) { + Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); + if (resource.isPresent()) { + log.debug("Resource found in temporal cache for Resource ID: {}", resourceID); + return resource; + } else { + return super.get(resourceID); + } + } + + @Override + public Optional getAssociated(P primary) { + return get(ResourceID.fromResource(primary)); + } + + @Override + public Optional getCachedValue(ResourceID resourceID) { + return get(resourceID); + } + + protected boolean temporalCacheHasResourceWithVersionAs(R resource) { + var resourceID = ResourceID.fromResource(resource); + var res = temporaryResourceCache.getResourceFromCache(resourceID); + return res.map(r -> { + boolean resVersionsEqual = r.getMetadata().getResourceVersion() + .equals(resource.getMetadata().getResourceVersion()); + log.debug("Resource found in temporal cache for id: {} resource versions equal: {}", + resourceID, resVersionsEqual); + return resVersionsEqual; + }).orElse(false); + } + + @Override + public Stream list(String namespace, Predicate predicate) { + return manager().list(namespace, predicate); + } + + ManagedInformerEventSource setTemporalResourceCache( + TemporaryResourceCache temporaryResourceCache) { + this.temporaryResourceCache = temporaryResourceCache; + return this; + } } 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 new file mode 100644 index 0000000000..64c93484b8 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -0,0 +1,110 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +/** + *

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

+ *

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

+ * + * @param resource to cache. + */ +public class TemporaryResourceCache { + + private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); + + private final Map cache = new ConcurrentHashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + private final ManagedInformerEventSource managedInformerEventSource; + + public TemporaryResourceCache(ManagedInformerEventSource managedInformerEventSource) { + this.managedInformerEventSource = managedInformerEventSource; + } + + public void removeResourceFromCache(T resource) { + lock.lock(); + try { + cache.remove(ResourceID.fromResource(resource)); + } finally { + lock.unlock(); + } + } + + public void unconditionallyCacheResource(T newResource) { + lock.lock(); + try { + cache.put(ResourceID.fromResource(newResource), newResource); + } finally { + lock.unlock(); + } + } + + public void putAddedResource(T newResource) { + lock.lock(); + try { + ResourceID resourceID = ResourceID.fromResource(newResource); + if (managedInformerEventSource.get(resourceID).isEmpty()) { + log.debug("Putting resource to cache with ID: {}", resourceID); + cache.put(ResourceID.fromResource(newResource), newResource); + } else { + log.debug("Won't put resource into cache found already informer cache: {}", resourceID); + } + } finally { + lock.unlock(); + } + } + + public void putUpdatedResource(T newResource, String previousResourceVersion) { + lock.lock(); + try { + var resourceId = ResourceID.fromResource(newResource); + var informerCacheResource = managedInformerEventSource.get(resourceId); + if (informerCacheResource.isEmpty()) { + log.debug("No cached value present for resource: {}", newResource); + return; + } + // if this is not true that means the cache was already updated + if (informerCacheResource.get().getMetadata().getResourceVersion() + .equals(previousResourceVersion)) { + log.debug("Putting resource to temporal cache with id: {}", resourceId); + cache.put(resourceId, newResource); + } else { + // if something is in cache it's surely obsolete now + cache.remove(resourceId); + } + } finally { + lock.unlock(); + } + } + + public Optional getResourceFromCache(ResourceID resourceID) { + try { + lock.lock(); + return Optional.ofNullable(cache.get(resourceID)); + } finally { + lock.unlock(); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java new file mode 100644 index 0000000000..e73244f7c6 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java @@ -0,0 +1,77 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import static org.mockito.Mockito.*; + +class KubernetesDependentResourceTest { + + // private InformerEventSource informerEventSourceMock = mock(InformerEventSource.class); + // private AssociatedSecondaryResourceIdentifier associatedResourceIdentifierMock = + // mock(AssociatedSecondaryResourceIdentifier.class); + // private ResourceMatcher resourceMatcherMock = mock(ResourceMatcher.class); + // private KubernetesDependentResource.ClientFacade clientFacadeMock = + // mock(KubernetesDependentResource.ClientFacade.class); + // + // KubernetesDependentResource kubernetesDependentResource = + // new KubernetesDependentResource() { + // { + // this.informerEventSource = informerEventSourceMock; + // this.resourceMatcher = resourceMatcherMock; + // this.clientFacade = clientFacadeMock; + // this.resourceUpdatePreProcessor = mock(ResourceUpdatePreProcessor.class); + // } + // + // @Override + // protected Object desired(HasMetadata primary, Context context) { + // return testResource(); + // } + // }; + // + // @BeforeEach + // public void setup() { + // InformerConfiguration informerConfigurationMock = mock(InformerConfiguration.class); + // when(informerEventSourceMock.getConfiguration()).thenReturn(informerConfigurationMock); + // when(informerConfigurationMock.getAssociatedResourceIdentifier()) + // .thenReturn(associatedResourceIdentifierMock); + // when(associatedResourceIdentifierMock.associatedSecondaryID(any())) + // .thenReturn(ResourceID.fromResource(testResource())); + // } + // + // @Test + // void updateCallsInformerJustUpdatedHandler() { + // when(resourceMatcherMock.match(any(), any(), any())).thenReturn(false); + // when(clientFacadeMock.replaceResource(any(), any(), any())).thenReturn(testResource()); + // when(informerEventSourceMock.getAssociated(any())).thenReturn(Optional.of(testResource())); + // + // kubernetesDependentResource.reconcile(primaryResource(), null); + // + // verify(informerEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any()); + // } + // + // @Test + // void createCallsInformerJustUpdatedHandler() { + // when(clientFacadeMock.createResource(any(), any(), any())).thenReturn(testResource()); + // when(informerEventSourceMock.getAssociated(any())).thenReturn(Optional.empty()); + // + // kubernetesDependentResource.reconcile(primaryResource(), null); + // + // verify(informerEventSourceMock, times(1)).handleRecentResourceAdd(any()); + // } + // + // TestCustomResource primaryResource() { + // TestCustomResource testCustomResource = new TestCustomResource(); + // testCustomResource.setMetadata(new ObjectMeta()); + // testCustomResource.getMetadata().setName("test"); + // testCustomResource.getMetadata().setNamespace("default"); + // return testCustomResource; + // } + // + // ConfigMap testResource() { + // ConfigMap configMap = new ConfigMap(); + // configMap.setMetadata(new ObjectMeta()); + // configMap.getMetadata().setName("test"); + // configMap.getMetadata().setNamespace("default"); + // configMap.getMetadata().setResourceVersion("0"); + // return configMap; + // } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index 5a3f73742e..c02a3b0da1 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 @@ -13,7 +13,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceCache; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; @@ -44,8 +43,6 @@ class EventProcessorTest { private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); - private ControllerResourceCache resourceCacheMock = - mock(ControllerResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private ControllerResourceEventSource controllerResourceEventSourceMock = mock(ControllerResourceEventSource.class); @@ -54,12 +51,9 @@ class EventProcessorTest { private EventProcessor eventProcessorWithRetry; @BeforeEach - public void setup() { - + void setup() { when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(controllerResourceEventSourceMock); - when(controllerResourceEventSourceMock.getResourceCache()).thenReturn(resourceCacheMock); - eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, null)); @@ -68,22 +62,22 @@ public void setup() { spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", GenericRetry.defaultLimitedExponentialRetry(), null)); eventProcessorWithRetry.start(); - when(eventProcessor.retryEventSource()).thenReturn(retryTimerEventSourceMock); when(eventProcessorWithRetry.retryEventSource()).thenReturn(retryTimerEventSourceMock); } @Test - public void dispatchesEventsIfNoExecutionInProgress() { + void dispatchesEventsIfNoExecutionInProgress() { eventProcessor.handleEvent(prepareCREvent()); verify(reconciliationDispatcherMock, timeout(50).times(1)).handleExecution(any()); } @Test - public void skipProcessingIfLatestCustomResourceNotInCache() { + void skipProcessingIfLatestCustomResourceNotInCache() { Event event = prepareCREvent(); - when(resourceCacheMock.get(event.getRelatedCustomResourceID())).thenReturn(Optional.empty()); + when(controllerResourceEventSourceMock.get(event.getRelatedCustomResourceID())) + .thenReturn(Optional.empty()); eventProcessor.handleEvent(event); @@ -91,7 +85,7 @@ public void skipProcessingIfLatestCustomResourceNotInCache() { } @Test - public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedException { + void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedException { ResourceID resourceUid = eventAlreadyUnderProcessing(); eventProcessor.handleEvent(nonCREvent(resourceUid)); @@ -101,7 +95,7 @@ public void ifExecutionInProgressWaitsUntilItsFinished() throws InterruptedExcep } @Test - public void schedulesAnEventRetryOnException() { + void schedulesAnEventRetryOnException() { TestCustomResource customResource = testCustomResource(); ExecutionScope executionScope = new ExecutionScope(customResource, null); @@ -115,7 +109,7 @@ public void schedulesAnEventRetryOnException() { } @Test - public void executesTheControllerInstantlyAfterErrorIfNewEventsReceived() { + void executesTheControllerInstantlyAfterErrorIfNewEventsReceived() { Event event = prepareCREvent(); TestCustomResource customResource = testCustomResource(); overrideData(event.getRelatedCustomResourceID(), customResource); @@ -142,7 +136,7 @@ public void executesTheControllerInstantlyAfterErrorIfNewEventsReceived() { } @Test - public void successfulExecutionResetsTheRetry() { + void successfulExecutionResetsTheRetry() { log.info("Starting successfulExecutionResetsTheRetry"); Event event = prepareCREvent(); @@ -182,7 +176,7 @@ public void successfulExecutionResetsTheRetry() { } @Test - public void scheduleTimedEventIfInstructedByPostExecutionControl() { + void scheduleTimedEventIfInstructedByPostExecutionControl() { var testDelay = 10000L; when(reconciliationDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); @@ -194,7 +188,7 @@ public void scheduleTimedEventIfInstructedByPostExecutionControl() { } @Test - public void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { + void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { var testDelay = 10000L; when(reconciliationDispatcherMock.handleExecution(any())) .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); @@ -207,7 +201,7 @@ public void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { } @Test - public void doNotFireEventsIfClosing() { + void doNotFireEventsIfClosing() { eventProcessor.stop(); eventProcessor.handleEvent(prepareCREvent()); @@ -215,58 +209,7 @@ public void doNotFireEventsIfClosing() { } @Test - public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var updatedCr = testCustomResource(crID); - updatedCr.getMetadata().setResourceVersion("2"); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(updatedCr)); - - verify(mockCREventSource, times(1)).whitelistNextEvent(eq(crID)); - } - - @Test - public void dontWhitelistsEventWhenOtherChangeDuringExecution() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var updatedCr = testCustomResource(crID); - updatedCr.getMetadata().setResourceVersion("2"); - var otherChangeCR = testCustomResource(crID); - otherChangeCR.getMetadata().setResourceVersion("3"); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(otherChangeCR)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(updatedCr)); - - verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); - } - - @Test - public void dontWhitelistsEventIfUpdatedEventInCache() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(cr)); - - verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); - } - - @Test - public void cancelScheduleOnceEventsOnSuccessfulExecution() { + void cancelScheduleOnceEventsOnSuccessfulExecution() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); var cr = testCustomResource(crID); @@ -277,12 +220,13 @@ public void cancelScheduleOnceEventsOnSuccessfulExecution() { } @Test - public void startProcessedMarkedEventReceivedBefore() { + void startProcessedMarkedEventReceivedBefore() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, metricsMock)); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(testCustomResource())); + when(controllerResourceEventSourceMock.get(eq(crID))) + .thenReturn(Optional.of(testCustomResource())); eventProcessor.handleEvent(new Event(crID)); verify(reconciliationDispatcherMock, timeout(100).times(0)).handleExecution(any()); @@ -293,6 +237,19 @@ public void startProcessedMarkedEventReceivedBefore() { verify(metricsMock, times(1)).reconcileCustomResource(any(), isNull()); } + @Test + void updatesEventSourceHandlerIfResourceUpdated() { + TestCustomResource customResource = testCustomResource(); + ExecutionScope executionScope = new ExecutionScope(customResource, null); + PostExecutionControl postExecutionControl = + PostExecutionControl.customResourceUpdated(customResource); + + eventProcessorWithRetry.eventProcessingFinished(executionScope, postExecutionControl); + + + verify(controllerResourceEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any()); + } + private ResourceID eventAlreadyUnderProcessing() { when(reconciliationDispatcherMock.handleExecution(any())) .then( @@ -311,7 +268,7 @@ private ResourceEvent prepareCREvent() { private ResourceEvent prepareCREvent(ResourceID uid) { TestCustomResource customResource = testCustomResource(uid); - when(resourceCacheMock.get(eq(uid))).thenReturn(Optional.of(customResource)); + when(controllerResourceEventSourceMock.get(eq(uid))).thenReturn(Optional.of(customResource)); return new ResourceEvent(ResourceAction.UPDATED, ResourceID.fromResource(customResource)); } 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 83528256df..9782948dba 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 @@ -12,7 +12,6 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -99,27 +98,6 @@ public void eventWithNoGenerationProcessedIfNoFinalizer() { verify(eventHandler, times(1)).handleEvent(any()); } - @Test - public void handlesNextEventIfWhitelisted() { - TestCustomResource customResource = TestUtils.testCustomResource(); - customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - source.whitelistNextEvent(ResourceID.fromResource(customResource)); - - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); - - verify(eventHandler, times(1)).handleEvent(any()); - } - - @Test - public void notHandlesNextEventIfNotWhitelisted() { - TestCustomResource customResource = TestUtils.testCustomResource(); - customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); - - verify(eventHandler, times(0)).handleEvent(any()); - } - @Test public void callsBroadcastsOnResourceEvents() { TestCustomResource customResource1 = TestUtils.testCustomResource(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java new file mode 100644 index 0000000000..3e25e1da3c --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorderTest.java @@ -0,0 +1,80 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +import static org.assertj.core.api.Assertions.assertThat; + +class EventRecorderTest { + + public static final String RESOURCE_VERSION = "0"; + public static final String RESOURCE_VERSION1 = "1"; + EventRecorder eventRecorder = new EventRecorder(); + + ConfigMap testConfigMap = testConfigMap(RESOURCE_VERSION); + ConfigMap testConfigMap2 = testConfigMap(RESOURCE_VERSION1); + + ResourceID id = ResourceID.fromResource(testConfigMap); + + @Test + void recordsEvents() { + + assertThat(eventRecorder.isRecordingFor(id)).isFalse(); + + eventRecorder.startEventRecording(id); + assertThat(eventRecorder.isRecordingFor(id)).isTrue(); + + eventRecorder.recordEvent(testConfigMap); + + eventRecorder.stopEventRecording(id); + assertThat(eventRecorder.isRecordingFor(id)).isFalse(); + } + + @Test + void getsLastRecorded() { + eventRecorder.startEventRecording(id); + + eventRecorder.recordEvent(testConfigMap); + eventRecorder.recordEvent(testConfigMap2); + + assertThat(eventRecorder.getLastEvent(id)).isEqualTo(testConfigMap2); + } + + @Test + void checksContainsWithResourceVersion() { + eventRecorder.startEventRecording(id); + + eventRecorder.recordEvent(testConfigMap); + eventRecorder.recordEvent(testConfigMap2); + + assertThat(eventRecorder.containsEventWithResourceVersion(id, RESOURCE_VERSION)).isTrue(); + assertThat(eventRecorder.containsEventWithResourceVersion(id, RESOURCE_VERSION1)).isTrue(); + assertThat(eventRecorder.containsEventWithResourceVersion(id, "xxx")).isFalse(); + } + + @Test + void checkLastItemVersion() { + eventRecorder.startEventRecording(id); + + eventRecorder.recordEvent(testConfigMap); + eventRecorder.recordEvent(testConfigMap2); + + assertThat(eventRecorder.containsEventWithVersionButItsNotLastOne(id, RESOURCE_VERSION)) + .isTrue(); + assertThat(eventRecorder.containsEventWithVersionButItsNotLastOne(id, RESOURCE_VERSION1)) + .isFalse(); + } + + ConfigMap testConfigMap(String resourceVersion) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName("test"); + configMap.getMetadata().setResourceVersion(resourceVersion); + + return configMap; + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java new file mode 100644 index 0000000000..1c901034e0 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -0,0 +1,194 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.FilterWatchListMultiDeletable; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class InformerEventSourceTest { + + private static final String PREV_RESOURCE_VERSION = "0"; + private static final String DEFAULT_RESOURCE_VERSION = "1"; + private static final String NEXT_RESOURCE_VERSION = "2"; + + private InformerEventSource informerEventSource; + private KubernetesClient clientMock = mock(KubernetesClient.class); + private TemporaryResourceCache temporaryResourceCacheMock = + mock(TemporaryResourceCache.class); + private EventHandler eventHandlerMock = mock(EventHandler.class); + private MixedOperation crClientMock = mock(MixedOperation.class); + private FilterWatchListMultiDeletable specificResourceClientMock = + mock(FilterWatchListMultiDeletable.class); + private FilterWatchListDeletable labeledResourceClientMock = mock(FilterWatchListDeletable.class); + private SharedIndexInformer informer = mock(SharedIndexInformer.class); + private InformerConfiguration informerConfiguration = + mock(InformerConfiguration.class); + + @BeforeEach + void setup() { + when(clientMock.resources(any())).thenReturn(crClientMock); + when(crClientMock.inAnyNamespace()).thenReturn(specificResourceClientMock); + when(specificResourceClientMock.withLabelSelector((String) null)) + .thenReturn(labeledResourceClientMock); + when(labeledResourceClientMock.runnableInformer(0)).thenReturn(informer); + + when(informerConfiguration.getPrimaryResourcesRetriever()) + .thenReturn(mock(PrimaryResourcesRetriever.class)); + + informerEventSource = new InformerEventSource<>(informerConfiguration, clientMock); + informerEventSource.setTemporalResourceCache(temporaryResourceCacheMock); + informerEventSource.setEventHandler(eventHandlerMock); + + PrimaryResourcesRetriever primaryResourcesRetriever = mock(PrimaryResourcesRetriever.class); + when(informerConfiguration.getPrimaryResourcesRetriever()) + .thenReturn(primaryResourcesRetriever); + when(primaryResourcesRetriever.associatedPrimaryResources(any())) + .thenReturn(Set.of(ResourceID.fromResource(testDeployment()))); + } + + @Test + void skipsEventPropagationIfResourceWithSameVersionInResourceCache() { + when(temporaryResourceCacheMock.getResourceFromCache(any())) + .thenReturn(Optional.of(testDeployment())); + + informerEventSource.onAdd(testDeployment()); + informerEventSource.onUpdate(testDeployment(), testDeployment()); + + verify(eventHandlerMock, never()).handleEvent(any()); + } + + @Test + void propagateEventAndRemoveResourceFromTempCacheIfResourceVersionMismatch() { + Deployment cachedDeployment = testDeployment(); + cachedDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + when(temporaryResourceCacheMock.getResourceFromCache(any())) + .thenReturn(Optional.of(cachedDeployment)); + + + informerEventSource.onUpdate(cachedDeployment, testDeployment()); + + verify(eventHandlerMock, times(1)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(1)).removeResourceFromCache(any()); + } + + @Test + void notPropagatesEventIfAfterUpdateReceivedJustTheRelatedEvent() { + var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onUpdate(prevTestDeployment, testDeployment); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); + } + + + @Test + void notPropagatesEventIfAfterCreateReceivedJustTheRelatedEvent() { + var testDeployment = testDeployment(); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onAdd(testDeployment); + informerEventSource.handleRecentResourceCreate(testDeployment); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); + } + + @Test + void propagatesEventIfNewEventReceivedAfterTheCurrentTargetEvent() { + var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + var nextTestDeployment = testDeployment(); + nextTestDeployment.getMetadata().setResourceVersion(NEXT_RESOURCE_VERSION); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onUpdate(prevTestDeployment, testDeployment); + informerEventSource.onUpdate(testDeployment, nextTestDeployment); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(1)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); + } + + @Test + void notPropagatesEventIfMoreReceivedButTheLastIsTheUpdated() { + var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + var prevPrevTestDeployment = testDeployment(); + prevPrevTestDeployment.getMetadata().setResourceVersion("-1"); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment); + informerEventSource.onUpdate(prevTestDeployment, testDeployment); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); + } + + @Test + void putsResourceOnTempCacheIfNoEventRecorded() { + var testDeployment = testDeployment(); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any()); + } + + @Test + void putsResourceOnTempCacheIfNoEventRecordedWithSameResourceVersion() { + var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); + var prevPrevTestDeployment = testDeployment(); + prevPrevTestDeployment.getMetadata().setResourceVersion("-1"); + + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment); + informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + + verify(eventHandlerMock, times(0)).handleEvent(any()); + verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any()); + } + + Deployment testDeployment() { + Deployment deployment = new Deployment(); + deployment.setMetadata(new ObjectMeta()); + deployment.getMetadata().setResourceVersion(DEFAULT_RESOURCE_VERSION); + deployment.getMetadata().setName("test"); + return deployment; + } + +} 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 new file mode 100644 index 0000000000..2af66a4abe --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java @@ -0,0 +1,100 @@ +package io.javaoperatorsdk.operator.processing.event.source.informer; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TemporaryResourceCacheTest { + + public static final String RESOURCE_VERSION = "1"; + private InformerEventSource informerEventSource = mock(InformerEventSource.class); + private TemporaryResourceCache temporaryResourceCache = + new TemporaryResourceCache<>(informerEventSource); + + + @Test + void updateAddsTheResourceIntoCacheIfTheInformerHasThePreviousResourceVersion() { + var testResource = testResource(); + var prevTestResource = testResource(); + prevTestResource.getMetadata().setResourceVersion("0"); + when(informerEventSource.get(any())).thenReturn(Optional.of(prevTestResource)); + + temporaryResourceCache.putUpdatedResource(testResource, "0"); + + var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isPresent(); + } + + @Test + void updateNotAddsTheResourceIntoCacheIfTheInformerHasOtherVersion() { + var testResource = testResource(); + var informerCachedResource = testResource(); + informerCachedResource.getMetadata().setResourceVersion("x"); + when(informerEventSource.get(any())).thenReturn(Optional.of(informerCachedResource)); + + temporaryResourceCache.putUpdatedResource(testResource, "0"); + + var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isNotPresent(); + } + + @Test + void addOperationAddsTheResourceIfInformerCacheStillEmpty() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.empty()); + + temporaryResourceCache.putAddedResource(testResource); + + var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isPresent(); + } + + @Test + void addOperationNotAddsTheResourceIfInformerCacheNotEmpty() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.of(testResource())); + + temporaryResourceCache.putAddedResource(testResource); + + var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); + assertThat(cached).isNotPresent(); + } + + @Test + void removesResourceFromCache() { + ConfigMap testResource = propagateTestResourceToCache(); + + temporaryResourceCache.removeResourceFromCache(testResource()); + + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) + .isNotPresent(); + } + + private ConfigMap propagateTestResourceToCache() { + var testResource = testResource(); + when(informerEventSource.get(any())).thenReturn(Optional.empty()); + temporaryResourceCache.putAddedResource(testResource); + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) + .isPresent(); + return testResource; + } + + ConfigMap testResource() { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName("test"); + configMap.getMetadata().setNamespace("default"); + configMap.getMetadata().setResourceVersion(RESOURCE_VERSION); + return configMap; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java new file mode 100644 index 0000000000..57fbc78838 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java @@ -0,0 +1,88 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestCustomResource; +import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestCustomResourceSpec; +import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestReconciler; + +import static io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestReconciler.CONFIG_MAP_TEST_DATA_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class CreateUpdateDependentEventFilterIT { + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder() + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new CreateUpdateEventFilterTestReconciler()) + .build(); + + @Test + void updateEventNotReceivedAfterCreateOrUpdate() { + CreateUpdateEventFilterTestCustomResource resource = prepareTestResource(); + var createdResource = + operator.create(CreateUpdateEventFilterTestCustomResource.class, resource); + + await() + .atMost(Duration.ofSeconds(1)) + .until(() -> { + var cm = operator.get(ConfigMap.class, createdResource.getMetadata().getName()); + if (cm == null) { + return false; + } + return cm.getData() + .get(CONFIG_MAP_TEST_DATA_KEY) + .equals(createdResource.getSpec().getValue()); + }); + + assertThat( + ((CreateUpdateEventFilterTestReconciler) operator.getFirstReconciler()) + .getNumberOfExecutions()) + .isEqualTo(1); // this should be 1 usually but sometimes event is received + // faster than added resource added to the cache + + + CreateUpdateEventFilterTestCustomResource actualCreatedResource = + operator.get(CreateUpdateEventFilterTestCustomResource.class, + resource.getMetadata().getName()); + actualCreatedResource.getSpec().setValue("2"); + operator.replace(CreateUpdateEventFilterTestCustomResource.class, actualCreatedResource); + + + await().atMost(Duration.ofSeconds(1)) + .until(() -> { + var cm = operator.get(ConfigMap.class, createdResource.getMetadata().getName()); + if (cm == null) { + return false; + } + return cm.getData() + .get(CONFIG_MAP_TEST_DATA_KEY) + .equals(actualCreatedResource.getSpec().getValue()); + }); + + assertThat( + ((CreateUpdateEventFilterTestReconciler) operator.getFirstReconciler()) + .getNumberOfExecutions()) + // same as for previous assert (usually this should be 2) + .isEqualTo(2); + } + + private CreateUpdateEventFilterTestCustomResource prepareTestResource() { + CreateUpdateEventFilterTestCustomResource resource = + new CreateUpdateEventFilterTestCustomResource(); + resource.setMetadata(new ObjectMeta()); + resource.getMetadata().setName("test1"); + resource.setSpec(new CreateUpdateEventFilterTestCustomResourceSpec()); + resource.getSpec().setValue("1"); + return resource; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java similarity index 56% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index 20c0f20b80..d86385f1f1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.dependent; +package io.javaoperatorsdk.operator; import java.time.Duration; @@ -10,8 +10,10 @@ import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; +import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResourceSpec; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestReconciler; +import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class StandaloneDependentResourceIT { @@ -29,10 +31,38 @@ class StandaloneDependentResourceIT { void dependentResourceManagesDeployment() { StandaloneDependentTestCustomResource customResource = new StandaloneDependentTestCustomResource(); + customResource.setSpec(new StandaloneDependentTestCustomResourceSpec()); customResource.setMetadata(new ObjectMeta()); customResource.getMetadata().setName(DEPENDENT_TEST_NAME); var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + awaitForDeploymentReadyReplicas(1); + assertThat( + ((StandaloneDependentTestReconciler) operator.getFirstReconciler()).isErrorOccurred()) + .isFalse(); + } + + @Test + void executeUpdateForTestingCacheUpdateForGetResource() { + StandaloneDependentTestCustomResource customResource = + new StandaloneDependentTestCustomResource(); + customResource.setSpec(new StandaloneDependentTestCustomResourceSpec()); + customResource.setMetadata(new ObjectMeta()); + customResource.getMetadata().setName(DEPENDENT_TEST_NAME); + var createdCR = operator.create(StandaloneDependentTestCustomResource.class, customResource); + + awaitForDeploymentReadyReplicas(1); + + createdCR.getSpec().setReplicaCount(2); + operator.replace(StandaloneDependentTestCustomResource.class, createdCR); + + awaitForDeploymentReadyReplicas(2); + assertThat( + ((StandaloneDependentTestReconciler) operator.getFirstReconciler()).isErrorOccurred()) + .isFalse(); + } + + void awaitForDeploymentReadyReplicas(int expectedReplicaCount) { await() .pollInterval(Duration.ofMillis(300)) .atMost(Duration.ofSeconds(50)) @@ -42,13 +72,13 @@ void dependentResourceManagesDeployment() { operator .getKubernetesClient() .resources(Deployment.class) - .inNamespace(createdCR.getMetadata().getNamespace()) + .inNamespace(operator.getNamespace()) .withName(DEPENDENT_TEST_NAME) .get(); return deployment != null && deployment.getStatus() != null && deployment.getStatus().getReadyReplicas() != null - && deployment.getStatus().getReadyReplicas() > 0; + && deployment.getStatus().getReadyReplicas() == expectedReplicaCount; }); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResource.java new file mode 100644 index 0000000000..5797a44d9d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResource.java @@ -0,0 +1,22 @@ +package io.javaoperatorsdk.operator.sample.createupdateeventfilter; + +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("cue") +public class CreateUpdateEventFilterTestCustomResource + extends + CustomResource + implements Namespaced { + + @Override + protected CreateUpdateEventFilterTestCustomResourceStatus initStatus() { + return new CreateUpdateEventFilterTestCustomResourceStatus(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceSpec.java new file mode 100644 index 0000000000..fcd3807bc8 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceSpec.java @@ -0,0 +1,16 @@ +package io.javaoperatorsdk.operator.sample.createupdateeventfilter; + +public class CreateUpdateEventFilterTestCustomResourceSpec { + + private String value; + + public String getValue() { + return value; + } + + public CreateUpdateEventFilterTestCustomResourceSpec setValue(String value) { + this.value = value; + return this; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceStatus.java new file mode 100644 index 0000000000..6733e1a7cd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestCustomResourceStatus.java @@ -0,0 +1,7 @@ +package io.javaoperatorsdk.operator.sample.createupdateeventfilter; + +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; + +public class CreateUpdateEventFilterTestCustomResourceStatus extends ObservedGenerationAwareStatus { + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java new file mode 100644 index 0000000000..ae41eacb68 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -0,0 +1,120 @@ +package io.javaoperatorsdk.operator.sample.createupdateeventfilter; + +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +@ControllerConfiguration(finalizerName = NO_FINALIZER) +public class CreateUpdateEventFilterTestReconciler + implements Reconciler, + EventSourceInitializer, + KubernetesClientAware { + + public static final String CONFIG_MAP_TEST_DATA_KEY = "key"; + private KubernetesClient client; + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private InformerEventSource informerEventSource; + + @Override + public UpdateControl reconcile( + CreateUpdateEventFilterTestCustomResource resource, Context context) { + numberOfExecutions.incrementAndGet(); + + ConfigMap configMap = + client + .configMaps() + .inNamespace(resource.getMetadata().getNamespace()) + .withName(resource.getMetadata().getName()) + .get(); + if (configMap == null) { + var configMapToCreate = createConfigMap(resource); + try { + informerEventSource.prepareForCreateOrUpdateEventFiltering( + ResourceID.fromResource(configMapToCreate)); + configMap = + client + .configMaps() + .inNamespace(resource.getMetadata().getNamespace()) + .create(configMapToCreate); + informerEventSource.handleRecentResourceCreate(configMap); + } catch (RuntimeException e) { + informerEventSource + .cleanupOnCreateOrUpdateEventFiltering(ResourceID.fromResource(configMapToCreate)); + throw e; + } + } else { + if (!Objects.equals( + configMap.getData().get(CONFIG_MAP_TEST_DATA_KEY), resource.getSpec().getValue())) { + configMap.getData().put(CONFIG_MAP_TEST_DATA_KEY, resource.getSpec().getValue()); + try { + informerEventSource + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(configMap)); + var newConfigMap = + client + .configMaps() + .inNamespace(resource.getMetadata().getNamespace()) + .replace(configMap); + informerEventSource.handleRecentResourceUpdate( + newConfigMap, configMap.getMetadata().getResourceVersion()); + } catch (RuntimeException e) { + informerEventSource + .cleanupOnCreateOrUpdateEventFiltering(ResourceID.fromResource(configMap)); + throw e; + } + } + } + return UpdateControl.noUpdate(); + } + + private ConfigMap createConfigMap(CreateUpdateEventFilterTestCustomResource resource) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName(resource.getMetadata().getName()); + configMap.getMetadata().setLabels(new HashMap<>()); + configMap.getMetadata().getLabels().put("integrationtest", this.getClass().getSimpleName()); + configMap.getMetadata().setNamespace(resource.getMetadata().getNamespace()); + configMap.setData(new HashMap<>()); + configMap.getData().put(CONFIG_MAP_TEST_DATA_KEY, resource.getSpec().getValue()); + configMap.addOwnerReference(resource); + + return configMap; + } + + @Override + public List prepareEventSources( + EventSourceContext context) { + InformerConfiguration informerConfiguration = + InformerConfiguration.from(context, ConfigMap.class) + .withLabelSelector("integrationtest = " + this.getClass().getSimpleName()) + .build(); + informerEventSource = new InformerEventSource<>(informerConfiguration, client); + return List.of(informerEventSource); + } + + @Override + public KubernetesClient getKubernetesClient() { + return client; + } + + @Override + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.client = kubernetesClient; + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java index 3e6737c83c..e88d00c985 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResource.java @@ -10,6 +10,7 @@ @Version("v1") @ShortNames("sdt") public class StandaloneDependentTestCustomResource - extends CustomResource + extends + CustomResource implements Namespaced { } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java new file mode 100644 index 0000000000..4a88cf6044 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestCustomResourceSpec.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.standalonedependent; + +public class StandaloneDependentTestCustomResourceSpec { + + private int replicaCount; + + public StandaloneDependentTestCustomResourceSpec(int replicaCount) { + this.replicaCount = replicaCount; + } + + public StandaloneDependentTestCustomResourceSpec() { + this(1); + } + + public int getReplicaCount() { + return replicaCount; + } + + public StandaloneDependentTestCustomResourceSpec setReplicaCount(int replicaCount) { + this.replicaCount = replicaCount; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 21f02c6c3a..b6efb0f579 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -1,12 +1,14 @@ package io.javaoperatorsdk.operator.sample.standalonedependent; import java.util.List; +import java.util.Optional; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -17,11 +19,12 @@ public class StandaloneDependentTestReconciler implements Reconciler, EventSourceInitializer, - KubernetesClientAware { + KubernetesClientAware, ErrorStatusHandler { private KubernetesClient kubernetesClient; + private boolean errorOccurred = false; - KubernetesDependentResource deploymentDependent; + DeploymentDependentResource deploymentDependent; public StandaloneDependentTestReconciler() { deploymentDependent = new DeploymentDependentResource(); @@ -35,8 +38,17 @@ public List prepareEventSources( @Override public UpdateControl reconcile( - StandaloneDependentTestCustomResource resource, Context context) { - deploymentDependent.reconcile(resource, context); + StandaloneDependentTestCustomResource primary, Context context) { + deploymentDependent.reconcile(primary, context); + Optional deployment = deploymentDependent.getResource(primary); + if (deployment.isEmpty()) { + throw new IllegalStateException("Resource should not be empty after reconcile."); + } + + if (deployment.get().getSpec().getReplicas() != primary.getSpec().getReplicaCount()) { + // see https://github.com/java-operator-sdk/java-operator-sdk/issues/924 + throw new IllegalStateException("Something went wrong withe the cache mechanism."); + } return UpdateControl.noUpdate(); } @@ -51,15 +63,28 @@ public KubernetesClient getKubernetesClient() { return this.kubernetesClient; } + @Override + public Optional updateErrorStatus( + StandaloneDependentTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + errorOccurred = true; + return Optional.empty(); + } + + public boolean isErrorOccurred() { + return errorOccurred; + } + private static class DeploymentDependentResource extends KubernetesDependentResource - implements Creator { + implements Creator, + Updater { @Override protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { Deployment deployment = ReconcilerUtils.loadYaml(Deployment.class, getClass(), "nginx-deployment.yaml"); deployment.getMetadata().setName(primary.getMetadata().getName()); + deployment.getSpec().setReplicas(primary.getSpec().getReplicaCount()); deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); return deployment; } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index d7fd52f788..beef42599c 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.sample; -import java.time.Duration; import java.util.*; import org.apache.commons.lang3.StringUtils; @@ -20,7 +19,6 @@ import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; -import static org.awaitility.Awaitility.await; @ControllerConfiguration(finalizerName = NO_FINALIZER) public class WebPageReconciler @@ -59,7 +57,6 @@ public UpdateControl reconcile(WebPage webPage, Context context) { WebPageStatus status = new WebPageStatus(); - waitUntilConfigMapAvailable(webPage); status.setHtmlConfigMap(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()); status.setAreWeGood("Yes!"); status.setErrorMessage(null); @@ -68,12 +65,6 @@ public UpdateControl reconcile(WebPage webPage, Context context) { return UpdateControl.updateStatus(webPage); } - // todo after implemented we can remove this method: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/924 - private void waitUntilConfigMapAvailable(WebPage webPage) { - await().atMost(Duration.ofSeconds(5)).until(() -> configMapDR.getResource(webPage).isPresent()); - } - @Override public Optional updateErrorStatus( WebPage resource, RetryInfo retryInfo, RuntimeException e) { From a7eba4303f2962f79a137c8c35fc77d8075b1e5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 07:28:17 +0100 Subject: [PATCH 0319/1608] chore(deps): bump log4j.version from 2.17.1 to 2.17.2 (#974) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a8d47595aa..dfca9ec391 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ 5.8.2 5.12.0 1.7.36 - 2.17.1 + 2.17.2 4.3.1 3.12.0 1.0.1 From 019771fb7e0d58869bcd708111948ddef95bf795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 28 Feb 2022 16:15:48 +0100 Subject: [PATCH 0320/1608] Refactor WebPage Sample (#976) --- .../CrudKubernetesDependentResource.java | 17 ++ sample-operators/webpage/k8s/webpage.yaml | 4 +- .../operator/sample/WebPage.java | 8 + .../operator/sample/WebPageOperator.java | 7 +- .../operator/sample/WebPageReconciler.java | 280 ++++++++++-------- .../WebPageReconcilerDependentResources.java | 199 +++++++++++++ .../operator/sample/WebPageSpec.java | 7 + .../operator/sample/WebPageStatus.java | 9 + 8 files changed, 405 insertions(+), 126 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java new file mode 100644 index 0000000000..88e9bdc7ae --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; + +/** + * Adaptor Class for standalone mode for resources that manages Create, Update and Delete + * + * @param Managed resource + * @param

Primary Resource + */ +public abstract class CrudKubernetesDependentResource + extends + KubernetesDependentResource implements Creator, Updater, Deleter

{ +} diff --git a/sample-operators/webpage/k8s/webpage.yaml b/sample-operators/webpage/k8s/webpage.yaml index 382da972cb..d59c67eee1 100644 --- a/sample-operators/webpage/k8s/webpage.yaml +++ b/sample-operators/webpage/k8s/webpage.yaml @@ -1,6 +1,8 @@ apiVersion: "sample.javaoperatorsdk/v1" kind: WebPage metadata: + labels: + low-level: "true" name: hellows spec: html: | @@ -9,6 +11,6 @@ spec: Hello Operator World - Hello World! + Hello World! diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java index 6c10ffe3af..52ef6a938b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPage.java @@ -14,4 +14,12 @@ public class WebPage extends CustomResource protected WebPageStatus initStatus() { return new WebPageStatus(); } + + @Override + public String toString() { + return "WebPage{" + + "spec=" + spec + + ", status=" + status + + '}'; + } } 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 768fd26a72..b61b34334e 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,6 +1,7 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; +import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,7 +27,11 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, DefaultConfigurationService.instance()); - operator.register(new WebPageReconciler(client)); + if (Arrays.stream(args).anyMatch(arg -> arg.equals("--classic"))) { + operator.register(new WebPageReconciler(client)); + } else { + operator.register(new WebPageReconcilerDependentResources(client)); + } operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index beef42599c..b2727e065b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -9,125 +9,192 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.ReconcilerUtils; +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.dependent.Updater; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; -@ControllerConfiguration(finalizerName = NO_FINALIZER) +/** Shows how to implement reconciler using the low level api directly. */ +@ControllerConfiguration( + finalizerName = NO_FINALIZER, + labelSelector = WebPageReconciler.LOW_LEVEL_LABEL_KEY) public class WebPageReconciler implements Reconciler, ErrorStatusHandler, EventSourceInitializer { - private final Logger log = LoggerFactory.getLogger(getClass()); + public static final String LOW_LEVEL_LABEL_KEY = "low-level"; + public static final String INDEX_HTML = "index.html"; - private final KubernetesClient kubernetesClient; + private static final Logger log = LoggerFactory.getLogger(WebPageReconciler.class); - private KubernetesDependentResource configMapDR; - private KubernetesDependentResource deploymentDR; - private KubernetesDependentResource serviceDR; + private final KubernetesClient kubernetesClient; public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; - createDependentResources(kubernetesClient); } + InformerEventSource configMapEventSource; + @Override public List prepareEventSources(EventSourceContext context) { - return List.of( - configMapDR.eventSource(context), - deploymentDR.eventSource(context), - serviceDR.eventSource(context)); + configMapEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, ConfigMap.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + var deploymentEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, Deployment.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + var serviceEventSource = + new InformerEventSource<>(InformerConfiguration.from(context, Service.class) + .withLabelSelector(LOW_LEVEL_LABEL_KEY) + .build(), context); + return List.of(configMapEventSource, deploymentEventSource, serviceEventSource); } @Override public UpdateControl reconcile(WebPage webPage, Context context) { + log.info("Reconciling web page: {}", webPage); if (webPage.getSpec().getHtml().contains("error")) { throw new ErrorSimulationException("Simulating error"); } + String ns = webPage.getMetadata().getNamespace(); + String configMapName = configMapName(webPage); + String deploymentName = deploymentName(webPage); + + + ConfigMap desiredHtmlConfigMap = makeDesiredHtmlConfigMap(ns, configMapName, webPage); + Deployment desiredDeployment = + makeDesiredDeployment(webPage, deploymentName, ns, configMapName); + Service desiredService = makeDesiredService(webPage, ns, desiredDeployment); + + var previousConfigMap = context.getSecondaryResource(ConfigMap.class).orElse(null); + if (!match(desiredHtmlConfigMap, previousConfigMap)) { + log.info( + "Creating or updating ConfigMap {} in {}", + desiredHtmlConfigMap.getMetadata().getName(), + ns); + kubernetesClient.configMaps().inNamespace(ns).createOrReplace(desiredHtmlConfigMap); + } - configMapDR.reconcile(webPage, context); - deploymentDR.reconcile(webPage, context); - serviceDR.reconcile(webPage, context); + var existingDeployment = context.getSecondaryResource(Deployment.class).orElse(null); + if (!match(desiredDeployment, existingDeployment)) { + log.info( + "Creating or updating Deployment {} in {}", + desiredDeployment.getMetadata().getName(), + ns); + kubernetesClient.apps().deployments().inNamespace(ns).createOrReplace(desiredDeployment); + } - WebPageStatus status = new WebPageStatus(); + var existingService = context.getSecondaryResource(Service.class).orElse(null); + if (!match(desiredService, existingService)) { + log.info( + "Creating or updating Deployment {} in {}", + desiredDeployment.getMetadata().getName(), + ns); + kubernetesClient.services().inNamespace(ns).createOrReplace(desiredService); + } + + if (previousConfigMap != null && !StringUtils.equals( + previousConfigMap.getData().get(INDEX_HTML), + desiredHtmlConfigMap.getData().get(INDEX_HTML))) { + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient.pods().inNamespace(ns).withLabel("app", deploymentName(webPage)).delete(); + } + webPage.setStatus(createStatus(desiredHtmlConfigMap.getMetadata().getName())); + return UpdateControl.updateStatus(webPage); + } - status.setHtmlConfigMap(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName()); + private WebPageStatus createStatus(String configMapName) { + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(configMapName); status.setAreWeGood("Yes!"); status.setErrorMessage(null); - webPage.setStatus(status); + return status; + } - return UpdateControl.updateStatus(webPage); + private boolean match(Deployment desiredDeployment, Deployment deployment) { + if (deployment == null) { + return false; + } else { + return desiredDeployment.getSpec().getReplicas().equals(deployment.getSpec().getReplicas()) && + desiredDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage() + .equals( + deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); + } } - @Override - public Optional updateErrorStatus( - WebPage resource, RetryInfo retryInfo, RuntimeException e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); + private boolean match(Service desiredService, Service service) { + if (service == null) { + return false; + } + return desiredService.getSpec().getSelector().equals(service.getSpec().getSelector()); } - private void createDependentResources(KubernetesClient client) { - this.configMapDR = new ConfigMapDependentResource(); - - this.deploymentDR = - new KubernetesDependentResource<>() { - - @Override - protected Deployment desired(WebPage webPage, Context context) { - var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); - return deployment; - } - - @Override - protected Class resourceType() { - return Deployment.class; - } - }; - - this.serviceDR = - new KubernetesDependentResource<>() { - - @Override - protected Service desired(WebPage webPage, Context context) { - Service service = loadYaml(Service.class, getClass(), "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - Map labels = new HashMap<>(); - labels.put("app", deploymentName(webPage)); - service.getSpec().setSelector(labels); - return service; - } - - @Override - protected Class resourceType() { - return Service.class; - } - }; + private boolean match(ConfigMap desiredHtmlConfigMap, ConfigMap existingConfigMap) { + if (existingConfigMap == null) { + return false; + } else { + return desiredHtmlConfigMap.getData().equals(existingConfigMap.getData()); + } + } + + private Service makeDesiredService(WebPage webPage, String ns, Deployment desiredDeployment) { + Service desiredService = ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml"); + desiredService.getMetadata().setName(serviceName(webPage)); + desiredService.getMetadata().setNamespace(ns); + desiredService.getMetadata().setLabels(lowLevelLabel()); + desiredService + .getSpec() + .setSelector(desiredDeployment.getSpec().getTemplate().getMetadata().getLabels()); + desiredService.addOwnerReference(webPage); + return desiredService; + } + + private Deployment makeDesiredDeployment(WebPage webPage, String deploymentName, String ns, + String configMapName) { + Deployment desiredDeployment = + ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); + desiredDeployment.getMetadata().setName(deploymentName); + desiredDeployment.getMetadata().setNamespace(ns); + desiredDeployment.getMetadata().setLabels(lowLevelLabel()); + desiredDeployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + desiredDeployment.getSpec().getTemplate().getMetadata().getLabels().put("app", deploymentName); + desiredDeployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap(new ConfigMapVolumeSourceBuilder().withName(configMapName).build()); + desiredDeployment.addOwnerReference(webPage); + return desiredDeployment; + } + + private ConfigMap makeDesiredHtmlConfigMap(String ns, String configMapName, WebPage webPage) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + ConfigMap configMap = + new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName) + .withNamespace(ns) + .withLabels(lowLevelLabel()) + .build()) + .withData(data) + .build(); + configMap.addOwnerReference(webPage); + return configMap; + } + + private Map lowLevelLabel() { + Map labels = new HashMap<>(); + labels.put(LOW_LEVEL_LABEL_KEY, "true"); + return labels; } private static String configMapName(WebPage nginx) { @@ -142,45 +209,10 @@ private static String serviceName(WebPage nginx) { return nginx.getMetadata().getName(); } - private class ConfigMapDependentResource extends KubernetesDependentResource - implements - AssociatedSecondaryResourceIdentifier, Updater { - - @Override - protected ConfigMap desired(WebPage webPage, Context context) { - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - return new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(WebPageReconciler.configMapName(webPage)) - .withNamespace(webPage.getMetadata().getNamespace()) - .build()) - .withData(data) - .build(); - } - - @Override - public boolean match(ConfigMap actual, ConfigMap target, Context context) { - return StringUtils.equals( - actual.getData().get("index.html"), target.getData().get("index.html")); - } - - @Override - public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - super.update(actual, target, primary, context); - var ns = actual.getMetadata().getNamespace(); - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(primary)) - .delete(); - } - - @Override - public ResourceID associatedSecondaryID(WebPage primary) { - return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); - } + @Override + public Optional updateErrorStatus( + WebPage resource, RetryInfo retryInfo, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return Optional.of(resource); } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java new file mode 100644 index 0000000000..a313a0bf3d --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -0,0 +1,199 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CrudKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; + +/** + * Shows how to implement reconciler using standalone dependent resources. + */ +@ControllerConfiguration(finalizerName = NO_FINALIZER, + labelSelector = WebPageReconcilerDependentResources.DEPENDENT_RESOURCE_LABEL_SELECTOR) +public class WebPageReconcilerDependentResources + implements Reconciler, ErrorStatusHandler, EventSourceInitializer { + + public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level"; + private static final Logger log = + LoggerFactory.getLogger(WebPageReconcilerDependentResources.class); + private final KubernetesClient kubernetesClient; + + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; + + public WebPageReconcilerDependentResources(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + return List.of( + configMapDR.eventSource(context), + deploymentDR.eventSource(context), + serviceDR.eventSource(context)); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) { + if (webPage.getSpec().getHtml().contains("error")) { + // special case just to showcase error if doing a demo + throw new ErrorSimulationException("Simulating error"); + } + + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); + + webPage.setStatus( + createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); + return UpdateControl.updateStatus(webPage); + } + + private WebPageStatus createStatus(String configMapName) { + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(configMapName); + status.setAreWeGood("Yes!"); + status.setErrorMessage(null); + return status; + } + + @Override + public Optional updateErrorStatus( + WebPage resource, RetryInfo retryInfo, RuntimeException e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return Optional.of(resource); + } + + private void createDependentResources(KubernetesClient client) { + this.configMapDR = new ConfigMapDependentResource(); + this.configMapDR.setKubernetesClient(client); + configMapDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.deploymentDR = + new CrudKubernetesDependentResource<>() { + + @Override + protected Deployment desired(WebPage webPage, Context context) { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; + } + + @Override + protected Class resourceType() { + return Deployment.class; + } + }; + deploymentDR.setKubernetesClient(client); + deploymentDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.serviceDR = + new CrudKubernetesDependentResource<>() { + + @Override + protected Service desired(WebPage webPage, Context context) { + Service service = loadYaml(Service.class, getClass(), "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + } + + @Override + protected Class resourceType() { + return Service.class; + } + }; + serviceDR.setKubernetesClient(client); + serviceDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + } + + private static String configMapName(WebPage nginx) { + return nginx.getMetadata().getName() + "-html"; + } + + private static String deploymentName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private static String serviceName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + private class ConfigMapDependentResource + extends CrudKubernetesDependentResource + implements + AssociatedSecondaryResourceIdentifier { + + @Override + protected ConfigMap desired(WebPage webPage, Context context) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(WebPageReconcilerDependentResources.configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + } + + @Override + public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", ns); + kubernetesClient + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + } + + @Override + public ResourceID associatedSecondaryID(WebPage primary) { + return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java index db50be6c2d..562cda291d 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageSpec.java @@ -11,4 +11,11 @@ public String getHtml() { public void setHtml(String html) { this.html = html; } + + @Override + public String toString() { + return "WebPageSpec{" + + "html='" + html + '\'' + + '}'; + } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java index 2b2e2a23c8..3750e2f7c7 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java @@ -34,4 +34,13 @@ public WebPageStatus setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; return this; } + + @Override + public String toString() { + return "WebPageStatus{" + + "htmlConfigMap='" + htmlConfigMap + '\'' + + ", areWeGood='" + areWeGood + '\'' + + ", errorMessage='" + errorMessage + '\'' + + '}'; + } } From a4582a3f5b147964b500aa09a695a18b8d7dc8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Mar 2022 08:33:59 +0100 Subject: [PATCH 0321/1608] feat: dependent resource context + my sql e2e test improvements (#979) --- .../operator/api/reconciler/Context.java | 3 ++ .../api/reconciler/DefaultContext.java | 8 ++++ .../ManagedDependentResourceContext.java | 36 +++++++++++++++++ .../operator/processing/Controller.java | 5 +++ .../dependent/DependentResourceManager.java | 4 ++ .../operator/sample/MySQLSchemaOperator.java | 2 + .../sample/MySQLSchemaReconciler.java | 40 +++++-------------- .../{ => dependent}/ResourcePollerConfig.java | 4 +- .../SchemaDependentResource.java | 31 +++++++++----- .../SchemaPollingResourceSupplier.java | 4 +- .../SecretDependentResource.java | 26 +++++++++--- .../sample/MySQLSchemaOperatorE2E.java | 2 + 12 files changed, 118 insertions(+), 47 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/{ => dependent}/ResourcePollerConfig.java (78%) rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/{ => dependent}/SchemaDependentResource.java (77%) rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/{ => dependent}/SchemaPollingResourceSupplier.java (81%) rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/{ => dependent}/SecretDependentResource.java (59%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 6948d0f6a7..c2d853f38a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; public interface Context extends AttributeHolder { @@ -22,4 +23,6 @@ default T getMandatory(Object key, Class expectedType) { } ConfigurationService getConfigurationService(); + + ManagedDependentResourceContext managedDependentResourceContext(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 46ee5a3fcf..e4aa164d37 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -4,6 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; public class DefaultContext

extends MapAttributeHolder implements Context { @@ -12,12 +13,15 @@ public class DefaultContext

extends MapAttributeHolder im private final Controller

controller; private final P primaryResource; private final ConfigurationService configurationService; + private ManagedDependentResourceContext managedDependentResourceContext; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; this.configurationService = controller.getConfiguration().getConfigurationService(); + this.managedDependentResourceContext = new ManagedDependentResourceContext( + controller.getDependents()); } @Override @@ -36,4 +40,8 @@ public Optional getSecondaryResource(Class expectedType, String eventS public ConfigurationService getConfigurationService() { return configurationService; } + + public ManagedDependentResourceContext managedDependentResourceContext() { + return managedDependentResourceContext; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java new file mode 100644 index 0000000000..b2aa377aad --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import io.javaoperatorsdk.operator.OperatorException; + +public class ManagedDependentResourceContext { + + private List dependentResources; + + public ManagedDependentResourceContext(List dependentResources) { + this.dependentResources = dependentResources; + } + + public List getDependentResources() { + return Collections.unmodifiableList(dependentResources); + } + + public T getDependentResource(Class resourceClass) { + var resourceList = + dependentResources.stream() + .filter(dr -> dr.getClass().equals(resourceClass)) + .collect(Collectors.toList()); + if (resourceList.isEmpty()) { + throw new OperatorException( + "No dependent resource found for class: " + resourceClass.getName()); + } + if (resourceList.size() > 1) { + throw new OperatorException( + "More than one dependent resource found for class: " + resourceClass.getName()); + } + return (T) resourceList.get(0); + } +} 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 e8996c351c..a6c1979421 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 @@ -29,6 +29,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.dependent.DependentResourceManager; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -281,4 +282,8 @@ public void stop() { eventSourceManager.stop(); } } + + public List getDependents() { + return dependents.getDependents(); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index e860cbe577..3f46e12ca5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -105,4 +105,8 @@ private DependentResource createAndConfigureFrom(DependentResourceSpec dependent throw new IllegalStateException(e); } } + + public List getDependents() { + return dependents; + } } 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 150c72b391..8abf73f3f6 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 @@ -17,6 +17,8 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; +import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig; +import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; public class MySQLSchemaOperator { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 5952a1508f..a6923823aa 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -2,21 +2,22 @@ import java.util.Optional; -import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; -import io.javaoperatorsdk.operator.sample.schema.Schema; +import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; +import io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; +import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME; import static java.lang.String.format; // todo handle this, should work with finalizer @@ -26,42 +27,23 @@ @Dependent(type = SchemaDependentResource.class) }) public class MySQLSchemaReconciler - implements Reconciler, ErrorStatusHandler, - ContextInitializer { + implements Reconciler, ErrorStatusHandler { - static final String SECRET_FORMAT = "%s-secret"; - static final String USERNAME_FORMAT = "%s-user"; - - static final String MYSQL_SECRET_NAME = "mysql.secret.name"; - static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; - static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; - static final String BUILT_SCHEMA = "built schema"; static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class); public MySQLSchemaReconciler() {} - @Override - public void initContext(MySQLSchema primary, Context context) { - final var name = primary.getMetadata().getName(); - final var password = RandomStringUtils - .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here - - final var secretName = String.format(SECRET_FORMAT, name); - final var userName = String.format(USERNAME_FORMAT, name); - - // put information in context for other dependents and reconciler to use - context.put(MYSQL_SECRET_PASSWORD, password); - context.put(MYSQL_SECRET_NAME, secretName); - context.put(MYSQL_SECRET_USERNAME, userName); - } @Override public UpdateControl reconcile(MySQLSchema schema, Context context) { // we only need to update the status if we just built the schema, i.e. when it's present in the // context - return context.get(BUILT_SCHEMA, Schema.class).map(s -> { - updateStatusPojo(schema, context.getMandatory(MYSQL_SECRET_NAME, String.class), - context.getMandatory(MYSQL_SECRET_USERNAME, String.class)); + Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); + SchemaDependentResource schemaDependentResource = context.managedDependentResourceContext() + .getDependentResource(SchemaDependentResource.class); + return schemaDependentResource.getResource(schema).map(s -> { + updateStatusPojo(schema, secret.getMetadata().getName(), + secret.getData().get(MYSQL_SECRET_USERNAME)); log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); return UpdateControl.updateStatus(schema); }).orElse(UpdateControl.noUpdate()); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/ResourcePollerConfig.java similarity index 78% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/ResourcePollerConfig.java index 924c3978d1..5e9cc3f964 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/ResourcePollerConfig.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/ResourcePollerConfig.java @@ -1,4 +1,6 @@ -package io.javaoperatorsdk.operator.sample; +package io.javaoperatorsdk.operator.sample.dependent; + +import io.javaoperatorsdk.operator.sample.MySQLDbConfig; public class ResourcePollerConfig { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java similarity index 77% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java index b38d3cfdf8..0a97684e0d 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -1,10 +1,15 @@ -package io.javaoperatorsdk.operator.sample; +package io.javaoperatorsdk.operator.sample.dependent; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; +import java.util.Base64; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; @@ -14,9 +19,12 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.sample.*; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; +import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_PASSWORD; +import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME; import static java.lang.String.format; public class SchemaDependentResource @@ -26,6 +34,8 @@ public class SchemaDependentResource Creator, Deleter { + private static final Logger log = LoggerFactory.getLogger(SchemaDependentResource.class); + private MySQLDbConfig dbConfig; private int pollPeriod = 500; @@ -53,25 +63,22 @@ public Schema desired(MySQLSchema primary, Context context) { @Override public void create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { + Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); + var username = decode(secret.getData().get(MYSQL_SECRET_USERNAME)); + var password = decode(secret.getData().get(MYSQL_SECRET_PASSWORD)); final var schema = SchemaService.createSchemaAndRelatedUser( connection, target.getName(), - target.getCharacterSet(), - context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_USERNAME, String.class), - context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_PASSWORD, String.class)); - - // put the newly built schema in the context to let the reconciler know we just built it - context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema); + target.getCharacterSet(), username, password); } catch (SQLException e) { - MySQLSchemaReconciler.log.error("Error while creating Schema", e); + log.error("Error while creating Schema", e); throw new IllegalStateException(e); } } private Connection getConnection() throws SQLException { String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort()); - - MySQLSchemaReconciler.log.debug("Connecting to '{}' with user '{}'", connectURL, + log.debug("Connecting to '{}' with user '{}'", connectURL, dbConfig.getUser()); return DriverManager.getConnection(connectURL, dbConfig.getUser(), dbConfig.getPassword()); } @@ -99,4 +106,8 @@ public Optional getResource(MySQLSchema primaryResource) { } } + private static String decode(String value) { + return new String(Base64.getDecoder().decode(value.getBytes())); + } + } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java similarity index 81% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java index e7be1b3c42..acf620e819 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SchemaPollingResourceSupplier.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java @@ -1,8 +1,10 @@ -package io.javaoperatorsdk.operator.sample; +package io.javaoperatorsdk.operator.sample.dependent; import java.util.Optional; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.sample.MySQLDbConfig; +import io.javaoperatorsdk.operator.sample.MySQLSchema; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java similarity index 59% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 5dfa20a571..b118b07f0c 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -1,19 +1,27 @@ -package io.javaoperatorsdk.operator.sample; +package io.javaoperatorsdk.operator.sample.dependent; import java.util.Base64; +import org.apache.commons.lang3.RandomStringUtils; + import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.sample.MySQLSchema; import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*; public class SecretDependentResource extends KubernetesDependentResource - implements AssociatedSecondaryResourceIdentifier, Updater { + implements AssociatedSecondaryResourceIdentifier, Creator { + + public static final String SECRET_FORMAT = "%s-secret"; + public static final String USERNAME_FORMAT = "%s-user"; + public static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name"; + public static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password"; private static String encode(String value) { return Base64.getEncoder().encodeToString(value.getBytes()); @@ -21,15 +29,21 @@ private static String encode(String value) { @Override protected Secret desired(MySQLSchema schema, Context context) { + final var password = RandomStringUtils + .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here + final var name = schema.getMetadata().getName(); + final var secretName = String.format(SECRET_FORMAT, name); + final var userName = String.format(USERNAME_FORMAT, name); + return new SecretBuilder() .withNewMetadata() - .withName(context.getMandatory(MYSQL_SECRET_NAME, String.class)) + .withName(secretName) .withNamespace(schema.getMetadata().getNamespace()) .endMetadata() .addToData( - "MYSQL_USERNAME", encode(context.getMandatory(MYSQL_SECRET_USERNAME, String.class))) + MYSQL_SECRET_USERNAME, encode(userName)) .addToData( - "MYSQL_PASSWORD", encode(context.getMandatory(MYSQL_SECRET_PASSWORD, String.class))) + MYSQL_SECRET_PASSWORD, encode(password)) .build(); } 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 2ebece8aa2..8f9302a81a 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 @@ -21,6 +21,8 @@ import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig; +import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import static java.util.concurrent.TimeUnit.MINUTES; import static org.awaitility.Awaitility.await; From 67f972d26da9fa4cd84ce1a86ed554741c6b07ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Mar 2022 09:00:08 +0100 Subject: [PATCH 0322/1608] fix: remove map attribute holder (#981) --- .../api/reconciler/AttributeHolder.java | 12 -------- .../operator/api/reconciler/Context.java | 9 +----- .../api/reconciler/DefaultContext.java | 2 +- .../api/reconciler/EventSourceContext.java | 2 +- .../api/reconciler/MapAttributeHolder.java | 28 ------------------- 5 files changed, 3 insertions(+), 50 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java deleted file mode 100644 index 8fee3c2557..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/AttributeHolder.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -import java.util.Optional; - -public interface AttributeHolder { - - Optional get(Object key, Class expectedType); - - T getMandatory(Object key, Class expectedType); - - Optional put(Object key, Object value); -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index c2d853f38a..2f203fa07c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -5,7 +5,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; -public interface Context extends AttributeHolder { +public interface Context { Optional getRetryInfo(); @@ -15,13 +15,6 @@ default Optional getSecondaryResource(Class expectedType) { Optional getSecondaryResource(Class expectedType, String eventSourceName); - @Override - default T getMandatory(Object key, Class expectedType) { - return get(key, expectedType).orElseThrow(() -> new IllegalStateException( - "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() - + ") is missing or not of the expected type")); - } - ConfigurationService getConfigurationService(); ManagedDependentResourceContext managedDependentResourceContext(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index e4aa164d37..33f640789b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -7,7 +7,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; -public class DefaultContext

extends MapAttributeHolder implements Context { +public class DefaultContext

implements Context { private final RetryInfo retryInfo; private final Controller

controller; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index 026af88923..1a6c82bc6f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -11,7 +11,7 @@ * * @param

the type associated with the primary resource that is handled by your reconciler */ -public class EventSourceContext

extends MapAttributeHolder { +public class EventSourceContext

{ private final ResourceCache

primaryCache; private final ConfigurationService configurationService; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java deleted file mode 100644 index aee5ee7e54..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/MapAttributeHolder.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.javaoperatorsdk.operator.api.reconciler; - -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -public class MapAttributeHolder { - - private final ConcurrentHashMap attributes = new ConcurrentHashMap(); - - public Optional get(Object key, Class expectedType) { - return Optional.ofNullable(attributes.get(key)) - .filter(expectedType::isInstance) - .map(expectedType::cast); - } - - public Optional put(Object key, Object value) { - if (value == null) { - return Optional.ofNullable(attributes.remove(key)); - } - return Optional.ofNullable(attributes.put(key, value)); - } - - public T getMandatory(Object key, Class expectedType) { - return get(key, expectedType).orElseThrow(() -> new IllegalStateException( - "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() - + ") is missing or not of the expected type")); - } -} From 1e17e753b8adefd9a84b29c2150005465487fc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Mar 2022 12:25:09 +0100 Subject: [PATCH 0323/1608] feat: executor service override (#983) --- .../config/ConfigurationServiceOverrider.java | 16 ++++++++++++++++ .../sample/PureJavaApplicationRunner.java | 10 +++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index c9b60d2617..e2251eea64 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.api.config; import java.util.Set; +import java.util.concurrent.ExecutorService; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.Config; @@ -16,6 +17,7 @@ public class ConfigurationServiceOverrider { private Cloner cloner; private int timeoutSeconds; private boolean closeClientOnStop; + private ExecutorService executorService = null; public ConfigurationServiceOverrider( ConfigurationService original) { @@ -65,6 +67,11 @@ public ConfigurationServiceOverrider withCloseClientOnStop(boolean close) { return this; } + public ConfigurationServiceOverrider withExecutorService(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + public ConfigurationService build() { return new ConfigurationService() { @Override @@ -120,6 +127,15 @@ public Metrics getMetrics() { public boolean closeClientOnStop() { return closeClientOnStop; } + + @Override + public ExecutorService getExecutorService() { + if (executorService != null) { + return executorService; + } else { + return ConfigurationService.super.getExecutorService(); + } + } }; } diff --git a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java index 2cc212340b..19d0a471b7 100644 --- a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java +++ b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.sample; +import java.util.concurrent.Executors; + import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; @@ -8,9 +10,11 @@ public class PureJavaApplicationRunner { public static void main(String[] args) { Operator operator = - new Operator(ConfigurationServiceOverrider.override(DefaultConfigurationService.instance()) - .withConcurrentReconciliationThreads(2) - .build()); + new Operator( + ConfigurationServiceOverrider.override(DefaultConfigurationService.instance()) + .withExecutorService(Executors.newCachedThreadPool()) + .withConcurrentReconciliationThreads(2) + .build()); operator.register(new CustomServiceReconciler()); operator.start(); } From 0bd71d51d9637b6fa87460325928837de81a4429 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 1 Mar 2022 17:05:52 +0100 Subject: [PATCH 0324/1608] feat: re-add contextual map to context (#985) --- .../ManagedDependentResourceContext.java | 81 ++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java index b2aa377aad..e0c0060646 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java @@ -2,22 +2,97 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import io.javaoperatorsdk.operator.OperatorException; +/** + * Contextual information related to {@link DependentResource} either to retrieve the actual + * implementations to interact with them or to pass information between them and/or the reconciler + */ +@SuppressWarnings("rawtypes") public class ManagedDependentResourceContext { - private List dependentResources; + private final List dependentResources; + private final ConcurrentHashMap attributes = new ConcurrentHashMap(); + + /** + * Retrieve a contextual object, if it exists and is of the specified expected type, associated + * with the specified key. Contextual objects can be used to pass data between the reconciler and + * dependent resources and are scoped to the current reconciliation. + * + * @param key the key identifying which contextual object to retrieve + * @param expectedType the class representing the expected type of the contextual object + * @param the type of the expected contextual object + * @return an Optional containing the contextual object or {@link Optional#empty()} if no such + * object exists or doesn't match the expected type + */ + public Optional get(Object key, Class expectedType) { + return Optional.ofNullable(attributes.get(key)) + .filter(expectedType::isInstance) + .map(expectedType::cast); + } + + /** + * Associates the specified contextual value to the specified key. If the value is {@code null}, + * the semantics of this operation is defined as removing the mapping associated with the + * specified key. + * + * @param key the key identifying which contextual object to add or remove from the context + * @param value the value to add to the context or {@code null} to remove an existing entry + * associated with the specified key + * @return an Optional containing the previous value associated with the key or + * {@link Optional#empty()} if none existed + */ + @SuppressWarnings("unchecked") + public Optional put(Object key, Object value) { + if (value == null) { + return Optional.ofNullable(attributes.remove(key)); + } + return Optional.ofNullable(attributes.put(key, value)); + } + + /** + * Retrieves the value associated with the key or fail with an exception if none exists. + * + * @param key the key identifying which contextual object to retrieve + * @param expectedType the expected type of the value to retrieve + * @param the type of the expected contextual object + * @return the contextual object value associated with the specified key + * @see #get(Object, Class) + */ + public T getMandatory(Object key, Class expectedType) { + return get(key, expectedType).orElseThrow(() -> new IllegalStateException( + "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + + ") is missing or not of the expected type")); + } public ManagedDependentResourceContext(List dependentResources) { - this.dependentResources = dependentResources; + this.dependentResources = Collections.unmodifiableList(dependentResources); } + /** + * Retrieve all the known {@link DependentResource} implementations + * + * @return a list of known {@link DependentResource} implementations + */ public List getDependentResources() { - return Collections.unmodifiableList(dependentResources); + return dependentResources; } + /** + * Retrieve the dependent resource implementation associated with the specified resource type. + * + * @param resourceClass the dependent resource class for which we want to retrieve the associated + * dependent resource implementation + * @param the type of the resources for which we want to retrieve the associated dependent + * resource implementation + * @return the associated dependent resource implementation if it exists or an exception if it + * doesn't or several implementations are associated with the specified resource type + */ + @SuppressWarnings("unchecked") public T getDependentResource(Class resourceClass) { var resourceList = dependentResources.stream() From cd50a731432512eb1ca4d767054ebdef798d14cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 1 Mar 2022 17:16:35 +0100 Subject: [PATCH 0325/1608] docs: filter docs javadoc (#984) --- .../api/config/ControllerConfiguration.java | 17 +++++++++-------- .../api/reconciler/ControllerConfiguration.java | 5 ++++- 2 files changed, 13 insertions(+), 9 deletions(-) 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 eb247752aa..57a5648c5d 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 @@ -68,8 +68,7 @@ default boolean watchCurrentNamespace() { static boolean currentNamespaceWatched(Set namespaces) { return namespaces != null && namespaces.size() == 1 - && namespaces.contains( - Constants.WATCH_CURRENT_NAMESPACE); + && namespaces.contains(Constants.WATCH_CURRENT_NAMESPACE); } /** @@ -101,15 +100,17 @@ default RetryConfiguration getRetryConfiguration() { default void setConfigurationService(ConfigurationService service) {} default boolean useFinalizer() { - return !Constants.NO_FINALIZER - .equals(getFinalizer()); + return !Constants.NO_FINALIZER.equals(getFinalizer()); } /** - * Allow controllers to filter events before they are provided to the - * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. Note that the provided - * filter is combined with {@link #isGenerationAware()} to compute the final set of filters that - * should be applied; + * Allow controllers to filter events before they are passed to the + * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. + * + *

+ * Resource event filters only applies on events of the main custom resource. Not on events from + * other event sources nor the periodic events. + *

* * @return filter */ 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 bd8863cad1..cd63ba2153 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 @@ -51,7 +51,10 @@ /** - * Optional list of classes providing custom {@link ResourceEventFilter}. + *

+ * Resource event filters only applies on events of the main custom resource. Not on events from + * other event sources nor the periodic events. + *

* * @return the list of event filters. */ From f5806d7b8ac9c4a9fc6f481eac96161d556da97a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 07:52:04 +0100 Subject: [PATCH 0326/1608] chore(deps): bump actions/checkout from 2 to 3 (#988) --- .github/workflows/e2e-test.yml | 2 +- .github/workflows/pr.yml | 4 ++-- .github/workflows/release.yml | 8 ++++---- .github/workflows/snapshot-releases.yml | 4 ++-- .github/workflows/sonar.yml | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 31b89e1b20..fe24ca4343 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Minikube-Kubernetes uses: manusa/actions-setup-minikube@v2.4.3 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f6c5935cd0..2ca0df5fb2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,7 +14,7 @@ jobs: check_format_and_unit_tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: @@ -46,7 +46,7 @@ jobs: - java: 11 kubernetes: 'v1.22.1' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d250e8610a..99e7992f50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,11 +8,11 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: ${{ startsWith(github.event.release.tag_name, 'v1.' ) }} with: ref: "v1" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: ${{ startsWith(github.event.release.tag_name, 'v2.') }} - name: Set up Java and Maven uses: actions/setup-java@v2 @@ -53,11 +53,11 @@ jobs: update-working-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: ${{ startsWith(github.event.release.tag_name, 'v1.' ) }} with: ref: "v1" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: ${{ startsWith(github.event.release.tag_name, 'v2.') }} - name: Set up Java and Maven uses: actions/setup-java@v2 diff --git a/.github/workflows/snapshot-releases.yml b/.github/workflows/snapshot-releases.yml index df593393a6..67c13b3bfc 100644 --- a/.github/workflows/snapshot-releases.yml +++ b/.github/workflows/snapshot-releases.yml @@ -14,7 +14,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 05c7d6f753..1611625618 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest if: ${{ ( github.event_name == 'push' ) || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.owner.login == 'java-operator-sdk' ) }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Java and Maven uses: actions/setup-java@v2 with: From def54f51e3f7184de20ec15e256488eed9341f4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 07:52:54 +0100 Subject: [PATCH 0327/1608] chore(deps): bump actions/stale from 4.1.0 to 5 (#987) --- .github/workflows/stale-issues-and-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-issues-and-prs.yml b/.github/workflows/stale-issues-and-prs.yml index 604bc4fcd6..e5235ed13e 100644 --- a/.github/workflows/stale-issues-and-prs.yml +++ b/.github/workflows/stale-issues-and-prs.yml @@ -8,7 +8,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v4.1.0 + - uses: actions/stale@v5 with: days-before-issue-stale: 60 days-before-pr-stale: 60 From 19c473fddd715b47647f504258e220cbfbdd883d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 3 Mar 2022 09:23:40 +0100 Subject: [PATCH 0328/1608] fix: webpage e2e test (#982) --- .github/workflows/e2e-test.yml | 1 + .../operator/junit/E2EOperatorExtension.java | 11 +- sample-operators/webpage/k8s/operator.yaml | 27 ++--- sample-operators/webpage/pom.xml | 8 +- .../operator/sample/WebPageOperator.java | 7 +- .../operator/sample/WebPageReconciler.java | 4 +- .../WebPageReconcilerDependentResources.java | 10 +- .../operator/sample/WebPageStatus.java | 6 +- .../operator/sample/ingress.yaml | 19 --- .../sample/WebPageOperatorAbstractTest.java | 113 ++++++++++++++++++ .../WebPageOperatorDependentResourcesE2E.java | 35 ++++++ .../operator/sample/WebPageOperatorE2E.java | 62 ++++++++++ 12 files changed, 251 insertions(+), 52 deletions(-) delete mode 100644 sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml create mode 100644 sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java create mode 100644 sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java create mode 100644 sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 774df82b48..9c555b9e4b 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -16,6 +16,7 @@ jobs: sample_dir: - "sample-operators/mysql-schema" - "sample-operators/tomcat-operator" + - "sample-operators/webpage" runs-on: ubuntu-latest steps: - name: Checkout diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java index 91fecafc2e..55e6cc5c30 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/E2EOperatorExtension.java @@ -10,6 +10,7 @@ import java.util.Locale; import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.Logger; @@ -70,7 +71,7 @@ protected void before(ExtensionContext context) { } } - LOGGER.debug("Deploying the operator into Kubernetes"); + LOGGER.debug("Deploying the operator into Kubernetes. Target namespace: {}", namespace); operatorDeployment.forEach(hm -> { hm.getMetadata().setNamespace(namespace); if (hm.getKind().toLowerCase(Locale.ROOT).equals("clusterrolebinding")) { @@ -88,6 +89,7 @@ protected void before(ExtensionContext context) { kubernetesClient .resourceList(operatorDeployment) .waitUntilReady(operatorDeploymentTimeout.toMillis(), TimeUnit.MILLISECONDS); + LOGGER.debug("Operator resources deployed."); } @Override @@ -110,6 +112,13 @@ public Builder withDeploymentTimeout(Duration value) { return this; } + public Builder withOperatorDeployment(List hm, + Consumer> modifications) { + modifications.accept(hm); + operatorDeployment.addAll(hm); + return this; + } + public Builder withOperatorDeployment(List hm) { operatorDeployment.addAll(hm); return this; diff --git a/sample-operators/webpage/k8s/operator.yaml b/sample-operators/webpage/k8s/operator.yaml index cfdb1fe47c..0b673bf204 100644 --- a/sample-operators/webpage/k8s/operator.yaml +++ b/sample-operators/webpage/k8s/operator.yaml @@ -1,35 +1,27 @@ apiVersion: v1 -kind: Namespace -metadata: - name: webserver-operator - ---- -apiVersion: v1 kind: ServiceAccount metadata: - name: webserver-operator - namespace: webserver-operator + name: webpage-operator --- apiVersion: apps/v1 kind: Deployment metadata: - name: webserver-operator - namespace: webserver-operator + name: webpage-operator spec: selector: matchLabels: - app: webserver-operator + app: webpage-operator replicas: 1 template: metadata: labels: - app: webserver-operator + app: webpage-operator spec: - serviceAccountName: webserver-operator + serviceAccountName: webpage-operator containers: - name: operator - image: webserver-operator + image: webpage-operator imagePullPolicy: Never ports: - containerPort: 80 @@ -53,18 +45,17 @@ metadata: name: operator-admin subjects: - kind: ServiceAccount - name: webserver-operator - namespace: webserver-operator + name: webpage-operator roleRef: kind: ClusterRole - name: webserver-operator + name: webpage-operator apiGroup: "" --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: webserver-operator + name: webpage-operator rules: - apiGroups: - "" diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index f23c7407c5..93e32953c3 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -45,6 +45,12 @@ awaitility compile + + io.javaoperatorsdk + operator-framework-junit-5 + ${project.version} + test + @@ -57,7 +63,7 @@ gcr.io/distroless/java:11 - webserver-operator + webpage-operator
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 b61b34334e..0b643462b1 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,7 +1,6 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; -import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,16 +17,18 @@ import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class WebPageOperator { - + public static final String WEBPAGE_RECONCILER_ENV = "WEBPAGE_RECONCILER"; + public static final String WEBPAGE_RECONCILER_ENV_VALUE = "classic"; private static final Logger log = LoggerFactory.getLogger(WebPageOperator.class); + public static void main(String[] args) throws IOException { log.info("WebServer Operator starting!"); Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, DefaultConfigurationService.instance()); - if (Arrays.stream(args).anyMatch(arg -> arg.equals("--classic"))) { + if (WEBPAGE_RECONCILER_ENV_VALUE.equals(System.getenv(WEBPAGE_RECONCILER_ENV))) { operator.register(new WebPageReconciler(client)); } else { operator.register(new WebPageReconcilerDependentResources(client)); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index b2727e065b..90fb61251f 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -111,7 +111,7 @@ public UpdateControl reconcile(WebPage webPage, Context context) { private WebPageStatus createStatus(String configMapName) { WebPageStatus status = new WebPageStatus(); status.setHtmlConfigMap(configMapName); - status.setAreWeGood("Yes!"); + status.setAreWeGood(true); status.setErrorMessage(null); return status; } @@ -191,7 +191,7 @@ private ConfigMap makeDesiredHtmlConfigMap(String ns, String configMapName, WebP return configMap; } - private Map lowLevelLabel() { + public static Map lowLevelLabel() { Map labels = new HashMap<>(); labels.put(LOW_LEVEL_LABEL_KEY, "true"); return labels; diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index a313a0bf3d..cb5144a8d3 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -69,7 +69,7 @@ public UpdateControl reconcile(WebPage webPage, Context context) { private WebPageStatus createStatus(String configMapName) { WebPageStatus status = new WebPageStatus(); status.setHtmlConfigMap(configMapName); - status.setAreWeGood("Yes!"); + status.setAreWeGood(true); status.setErrorMessage(null); return status; } @@ -148,16 +148,16 @@ protected Class resourceType() { .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); } - private static String configMapName(WebPage nginx) { + public static String configMapName(WebPage nginx) { return nginx.getMetadata().getName() + "-html"; } - private static String deploymentName(WebPage nginx) { + public static String deploymentName(WebPage nginx) { return nginx.getMetadata().getName(); } - private static String serviceName(WebPage nginx) { - return nginx.getMetadata().getName(); + public static String serviceName(WebPage webPage) { + return webPage.getMetadata().getName(); } private class ConfigMapDependentResource diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java index 3750e2f7c7..5f66646b92 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStatus.java @@ -6,7 +6,7 @@ public class WebPageStatus extends ObservedGenerationAwareStatus { private String htmlConfigMap; - private String areWeGood; + private Boolean areWeGood; private String errorMessage; @@ -18,11 +18,11 @@ public void setHtmlConfigMap(String htmlConfigMap) { this.htmlConfigMap = htmlConfigMap; } - public String getAreWeGood() { + public Boolean getAreWeGood() { return areWeGood; } - public void setAreWeGood(String areWeGood) { + public void setAreWeGood(Boolean areWeGood) { this.areWeGood = areWeGood; } diff --git a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml b/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml deleted file mode 100644 index 6fd1ed93e9..0000000000 --- a/sample-operators/webpage/src/main/resources/io/javaoperatorsdk/operator/sample/ingress.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 -kind: Deployment -metadata: - name: -spec: - selector: - matchLabels: - app: - replicas: 1 - template: - metadata: - labels: - app: - spec: - containers: - - name: nginx - image: nginx:1.7.9 - ports: - - containerPort: 80 \ No newline at end of file diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java new file mode 100644 index 0000000000..092ad6f25a --- /dev/null +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java @@ -0,0 +1,113 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Objects; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.LocalPortForward; +import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; + +import static io.javaoperatorsdk.operator.sample.WebPageReconcilerDependentResources.serviceName; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public abstract class WebPageOperatorAbstractTest { + + static final Logger log = LoggerFactory.getLogger(WebPageOperatorDependentResourcesE2E.class); + + static final KubernetesClient client = new DefaultKubernetesClient(); + public static final String TEST_PAGE = "test-page"; + + boolean isLocal() { + String deployment = System.getProperty("test.deployment"); + boolean remote = (deployment != null && deployment.equals("remote")); + log.info("Running the operator " + (remote ? "remote" : "locally")); + return !remote; + } + + @Test + void testAddingWebPage() { + + var webPage = createWebPage(); + operator().create(WebPage.class, webPage); + + await() + .atMost(Duration.ofSeconds(20)) + .pollInterval(Duration.ofSeconds(1)) + .until( + () -> { + var actual = operator().get(WebPage.class, TEST_PAGE); + var deployment = operator().get(Deployment.class, + WebPageReconcilerDependentResources.deploymentName(webPage)); + + return Boolean.TRUE.equals(actual.getStatus().getAreWeGood()) + && Objects.equals(deployment.getSpec().getReplicas(), + deployment.getStatus().getReadyReplicas()); + }); + + String response = httpGetForWebPage(webPage); + assertThat(response).contains("Hello Operator World"); + } + + String httpGetForWebPage(WebPage webPage) { + LocalPortForward portForward = null; + try { + portForward = + client.services().inNamespace(webPage.getMetadata().getNamespace()) + .withName(serviceName(webPage)).portForward(80); + HttpClient httpClient = + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + HttpRequest request = + HttpRequest.newBuilder().GET() + .uri(new URI("http://localhost/" + portForward.getLocalPort())).build(); + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body(); + } catch (URISyntaxException | IOException | InterruptedException e) { + throw new IllegalStateException(e); + } finally { + if (portForward != null) { + try { + portForward.close(); + } catch (IOException e) { + log.error("Port forward close error.", e); + } + } + } + } + + WebPage createWebPage() { + WebPage webPage = new WebPage(); + webPage.setMetadata(new ObjectMeta()); + webPage.getMetadata().setName(TEST_PAGE); + webPage.getMetadata().setNamespace(operator().getNamespace()); + webPage.setSpec(new WebPageSpec()); + webPage + .getSpec() + .setHtml( + "\n" + + " \n" + + " Hello Operator World\n" + + " \n" + + " \n" + + " Hello World! \n" + + " \n" + + " "); + + return webPage; + } + + abstract AbstractOperatorExtension operator(); + +} diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java new file mode 100644 index 0000000000..580a2022c7 --- /dev/null +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java @@ -0,0 +1,35 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; +import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; +import io.javaoperatorsdk.operator.junit.OperatorExtension; + +class WebPageOperatorDependentResourcesE2E extends WebPageOperatorAbstractTest { + + public WebPageOperatorDependentResourcesE2E() throws FileNotFoundException {} + + @RegisterExtension + AbstractOperatorExtension operator = + isLocal() + ? OperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new WebPageReconcilerDependentResources(client)) + .build() + : E2EOperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get()) + .build(); + + @Override + AbstractOperatorExtension operator() { + return operator; + } +} diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java new file mode 100644 index 0000000000..a096b03965 --- /dev/null +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.sample; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; +import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; +import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; +import io.javaoperatorsdk.operator.junit.OperatorExtension; + +import static io.javaoperatorsdk.operator.sample.WebPageOperator.WEBPAGE_RECONCILER_ENV; +import static io.javaoperatorsdk.operator.sample.WebPageOperator.WEBPAGE_RECONCILER_ENV_VALUE; +import static io.javaoperatorsdk.operator.sample.WebPageReconciler.lowLevelLabel; + +class WebPageOperatorE2E extends WebPageOperatorAbstractTest { + + public WebPageOperatorE2E() throws FileNotFoundException {} + + @RegisterExtension + AbstractOperatorExtension operator = + isLocal() + ? OperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withReconciler(new WebPageReconciler(client)) + .build() + : E2EOperatorExtension.builder() + .waitForNamespaceDeletion(false) + .withConfigurationService(DefaultConfigurationService.instance()) + .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get(), + resources -> { + Deployment deployment = (Deployment) resources.stream() + .filter(r -> r instanceof Deployment).findFirst().orElseThrow(); + Container container = + deployment.getSpec().getTemplate().getSpec().getContainers().get(0); + if (container.getEnv() == null) { + container.setEnv(new ArrayList<>()); + } + container.getEnv().add( + new EnvVar(WEBPAGE_RECONCILER_ENV, WEBPAGE_RECONCILER_ENV_VALUE, null)); + }) + .build(); + + + @Override + AbstractOperatorExtension operator() { + return operator; + } + + @Override + WebPage createWebPage() { + WebPage page = super.createWebPage(); + page.getMetadata().setLabels(lowLevelLabel()); + return page; + } +} From 7433c8bb89faf10a5e1a93cf4a53022a6b991be2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Mar 2022 10:03:18 +0100 Subject: [PATCH 0329/1608] chore(deps): bump formatter-maven-plugin from 2.17.1 to 2.18.0 (#989) --- .../operator/api/ObservedGenerationAware.java | 2 +- .../api/config/ConfigurationService.java | 2 +- .../api/reconciler/EventSourceContext.java | 4 +- .../reconciler/EventSourceInitializer.java | 4 +- .../processing/event/source/EventSource.java | 4 +- .../source/informer/InformerEventSource.java | 131 ++++++++++-------- .../source/polling/PollingEventSource.java | 45 +++--- pom.xml | 2 +- 8 files changed, 105 insertions(+), 89 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java index 43927b8136..069953a32d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/ObservedGenerationAware.java @@ -15,7 +15,7 @@ * {@link UpdateControl#updateResource(HasMetadata)} is called. Although those results call normally * does not result in a status update, there will be a subsequent status update Kubernetes API call * in this case. - * + * * @see ObservedGenerationAwareStatus */ public interface ObservedGenerationAware { 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 2e4679efd3..49ccc1da07 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 @@ -92,7 +92,7 @@ default int concurrentReconciliationThreads() { /** * Used to clone custom resources. - * + * * @return the ObjectMapper to use */ default Cloner getResourceCloner() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index 0a93d33d40..ba32f493ec 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -39,7 +39,7 @@ public ResourceCache

getPrimaryCache() { * {@link io.javaoperatorsdk.operator.api.monitoring.Metrics} or * {@link io.javaoperatorsdk.operator.api.config.Cloner} implementations, which could be useful to * event sources. - * + * * @return the {@link ConfigurationService} associated with the operator */ public ConfigurationService getConfigurationService() { @@ -49,7 +49,7 @@ public ConfigurationService getConfigurationService() { /** * Provides access to the {@link KubernetesClient} used by the current * {@link io.javaoperatorsdk.operator.Operator} instance. - * + * * @return the {@link KubernetesClient} used by the current * {@link io.javaoperatorsdk.operator.Operator} instance. */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java index 79b15380a5..562ddc3b8f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceInitializer.java @@ -8,14 +8,14 @@ /** * An interface that a {@link Reconciler} can implement to have the SDK register the provided * {@link EventSource} - * + * * @param

the primary resource type handled by the associated {@link Reconciler} */ public interface EventSourceInitializer

{ /** * Prepares a list of {@link EventSource} implementations to be registered by the SDK. - * + * * @param context a {@link EventSourceContext} providing access to information useful to event * sources * @return list of event sources to register 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 04e19f7ceb..10a690c0e8 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 @@ -15,7 +15,7 @@ public interface EventSource extends LifecycleAware { /** * An optional name for your EventSource. This is only required if you need to register multiple * EventSources for the same resource type (e.g. {@code Deployment}). - * + * * @return the name associated with this EventSource */ default String name() { @@ -25,7 +25,7 @@ default String name() { /** * Sets the {@link EventHandler} that is linked to your reconciler when this EventSource is * registered. - * + * * @param handler the {@link EventHandler} associated with your reconciler */ void setEventHandler(EventHandler handler); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index eada096713..95ed3b6ef0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -22,8 +22,7 @@ import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; public class InformerEventSource - extends AbstractResourceEventSource - implements ResourceCache { + extends AbstractResourceEventSource implements ResourceCache { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); @@ -32,33 +31,46 @@ public class InformerEventSource private final AssociatedSecondaryResourceIdentifier

associatedWith; private final boolean skipUpdateEventPropagationIfNoChange; - public InformerEventSource(SharedInformer sharedInformer, + public InformerEventSource( + SharedInformer sharedInformer, PrimaryResourcesRetriever resourceToTargetResourceIDSet) { this(sharedInformer, resourceToTargetResourceIDSet, null, true); } - public InformerEventSource(KubernetesClient client, Class type, + public InformerEventSource( + KubernetesClient client, + Class type, PrimaryResourcesRetriever resourceToTargetResourceIDSet) { this(client, type, resourceToTargetResourceIDSet, false); } - public InformerEventSource(KubernetesClient client, Class type, + public InformerEventSource( + KubernetesClient client, + Class type, PrimaryResourcesRetriever resourceToTargetResourceIDSet, AssociatedSecondaryResourceIdentifier

associatedWith, boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, + this( + client.informers().sharedIndexInformerFor(type, 0), + resourceToTargetResourceIDSet, associatedWith, skipUpdateEventPropagationIfNoChange); } - InformerEventSource(KubernetesClient client, Class type, + InformerEventSource( + KubernetesClient client, + Class type, PrimaryResourcesRetriever resourceToTargetResourceIDSet, boolean skipUpdateEventPropagationIfNoChange) { - this(client.informers().sharedIndexInformerFor(type, 0), resourceToTargetResourceIDSet, null, + this( + client.informers().sharedIndexInformerFor(type, 0), + resourceToTargetResourceIDSet, + null, skipUpdateEventPropagationIfNoChange); } - public InformerEventSource(SharedInformer sharedInformer, + public InformerEventSource( + SharedInformer sharedInformer, PrimaryResourcesRetriever resourceToTargetResourceIDSet, AssociatedSecondaryResourceIdentifier

associatedWith, boolean skipUpdateEventPropagationIfNoChange) { @@ -68,41 +80,44 @@ public InformerEventSource(SharedInformer sharedInformer, this.skipUpdateEventPropagationIfNoChange = skipUpdateEventPropagationIfNoChange; if (sharedInformer.isRunning()) { log.warn( - "Informer is already running on event source creation, this is not desirable and may " + - "lead to non deterministic behavior."); + "Informer is already running on event source creation, this is not desirable and may " + + "lead to non deterministic behavior."); } this.associatedWith = Objects.requireNonNullElseGet(associatedWith, () -> ResourceID::fromResource); - sharedInformer.addEventHandler(new ResourceEventHandler<>() { - @Override - public void onAdd(T t) { - propagateEvent(t); - } - - @Override - public void onUpdate(T oldObject, T newObject) { - if (newObject == null) { - // this is a fix for this potential issue with informer: - // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 - propagateEvent(oldObject); - return; - } - - if (InformerEventSource.this.skipUpdateEventPropagationIfNoChange && - oldObject.getMetadata().getResourceVersion() - .equals(newObject.getMetadata().getResourceVersion())) { - return; - } - propagateEvent(newObject); - } - - @Override - public void onDelete(T t, boolean b) { - propagateEvent(t); - } - }); + sharedInformer.addEventHandler( + new ResourceEventHandler<>() { + @Override + public void onAdd(T t) { + propagateEvent(t); + } + + @Override + public void onUpdate(T oldObject, T newObject) { + if (newObject == null) { + // this is a fix for this potential issue with informer: + // https://github.com/java-operator-sdk/java-operator-sdk/issues/830 + propagateEvent(oldObject); + return; + } + + if (InformerEventSource.this.skipUpdateEventPropagationIfNoChange + && oldObject + .getMetadata() + .getResourceVersion() + .equals(newObject.getMetadata().getResourceVersion())) { + return; + } + propagateEvent(newObject); + } + + @Override + public void onDelete(T t, boolean b) { + propagateEvent(t); + } + }); } private void propagateEvent(T object) { @@ -110,18 +125,19 @@ private void propagateEvent(T object) { if (primaryResourceIdSet.isEmpty()) { return; } - primaryResourceIdSet.forEach(resourceId -> { - Event event = new Event(resourceId); - /* - * In fabric8 client for certain cases informers can be created on in a way that they are - * automatically started, what would cause a NullPointerException here, since an event might - * be received between creation and registration. - */ - final EventHandler eventHandler = getEventHandler(); - if (eventHandler != null) { - eventHandler.handleEvent(event); - } - }); + primaryResourceIdSet.forEach( + resourceId -> { + Event event = new Event(resourceId); + /* + * In fabric8 client for certain cases informers can be created on in a way that they are + * automatically started, what would cause a NullPointerException here, since an event + * might be received between creation and registration. + */ + final EventHandler eventHandler = getEventHandler(); + if (eventHandler != null) { + eventHandler.handleEvent(event); + } + }); } @Override @@ -141,7 +157,7 @@ private Store getStore() { /** * Retrieves the informed resource associated with the specified primary resource as defined by * the function provided when this InformerEventSource was created - * + * * @param resource the primary resource we want to retrieve the associated resource for * @return the informed resource associated with the specified primary resource */ @@ -150,17 +166,18 @@ public Optional getAssociated(P resource) { return get(id); } - public SharedInformer getSharedInformer() { return sharedInformer; } @Override public Optional get(ResourceID resourceID) { - return Optional.ofNullable(sharedInformer.getStore() - .getByKey(io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( - resourceID.getNamespace().orElse(null), - resourceID.getName()))); + return Optional.ofNullable( + sharedInformer + .getStore() + .getByKey( + io.fabric8.kubernetes.client.informers.cache.Cache.namespaceKeyFunc( + resourceID.getNamespace().orElse(null), resourceID.getName()))); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java index 4702bd8c38..f9a4265c04 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 @@ -12,29 +12,26 @@ import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; /** - *

* Polls resource (on contrary to {@link PerResourcePollingEventSource}) not per resource bases but * instead to calls supplier periodically and independently of the number of state of custom * resources managed by the operator. It is called on start (synced). This means that when the * reconciler first time executed on startup a poll already happened before. So if the cache does * not contain the target resource it means it is not created yet or was deleted while an operator * was not running. - *

+ * *

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

- * So the generic workflow in reconciler should be: + * execution. See {@link #put(ResourceID, Object)} method. So the generic workflow in reconciler + * should be: * *
    - *
  • Check if the cache contains the resource.
  • + *
  • Check if the cache contains the resource. *
  • If cache contains the resource reconcile it - compare with target state, update if necessary - *
  • - *
  • if cache not contains the resource create it.
  • + *
  • if cache not contains the resource create it. *
  • If the resource was created or updated, put the new version of the resource manually to the - * cache.
  • + * cache. *
* * @param type of the polled resource @@ -48,8 +45,8 @@ public class PollingEventSource extends CachingEventSo private final Supplier> supplierToPoll; private final long period; - public PollingEventSource(Supplier> supplier, - long period, Class resourceClass) { + public PollingEventSource( + Supplier> supplier, long period, Class resourceClass) { super(resourceClass); this.supplierToPoll = supplier; this.period = period; @@ -59,16 +56,19 @@ public PollingEventSource(Supplier> supplier, public void start() throws OperatorException { super.start(); getStateAndFillCache(); - timer.schedule(new TimerTask() { - @Override - public void run() { - if (!isRunning()) { - log.debug("Event source not yet started. Will not run."); - return; - } - getStateAndFillCache(); - } - }, period, period); + timer.schedule( + new TimerTask() { + @Override + public void run() { + if (!isRunning()) { + log.debug("Event source not yet started. Will not run."); + return; + } + getStateAndFillCache(); + } + }, + period, + period); } protected void getStateAndFillCache() { @@ -89,7 +89,7 @@ public void stop() throws OperatorException { /** * See {@link PerResourcePollingEventSource} for more info. - * + * * @param primary custom resource * @return related resource */ @@ -97,5 +97,4 @@ public void stop() throws OperatorException { public Optional getAssociated(P primary) { return getCachedValue(ResourceID.fromResource(primary)); } - } diff --git a/pom.xml b/pom.xml index dfca9ec391..5412cfac2a 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 2.8.2 2.5.2 5.0.0 - 2.17.1 + 2.18.0 1.0 1.6.2 From 57a29abcb2bb290b568f94ae7e7ca9239abaa233 Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 3 Mar 2022 10:08:24 +0100 Subject: [PATCH 0330/1608] fix: merge from master --- .../dependent/kubernetes/KubernetesDependentResource.java | 2 +- .../processing/event/source/informer/InformerEventSource.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 8ce3560fdc..bbdf74da5c 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 @@ -80,7 +80,7 @@ private void configureWith(ConfigurationService configService, String labelSelec /** * Use to share informers between event more resources. - * + * * @param informerEventSource informer to use * @param addOwnerReference to the created resource */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 2f8705b63a..006564426e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -224,7 +224,7 @@ public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resou /** * Mean to be called to clean up in case of an exception from the client. Usually in a catch * block. - * + * * @param resourceID of the resource */ public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID) { From 4aa7b80a4ce1784b57b91672df5661bb7b5a5271 Mon Sep 17 00:00:00 2001 From: csviri Date: Thu, 3 Mar 2022 10:09:08 +0100 Subject: [PATCH 0331/1608] fix: formatting issue for new version for formatter plugin --- .../operator/api/reconciler/ControllerConfiguration.java | 4 ++-- .../dependent/ManagedDependentResourceContext.java | 8 ++++---- .../event/source/informer/TemporaryResourceCache.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) 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 f71ef5c4f0..0feaa34b2d 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 @@ -62,7 +62,7 @@ /** * Optional configuration of the maximal interval the SDK will wait for a reconciliation request * to happen before one will be automatically triggered. - * + * * @return the maximal interval configuration */ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMaxInterval( @@ -71,7 +71,7 @@ ReconciliationMaxInterval reconciliationMaxInterval() default @ReconciliationMax /** * Optional list of {@link Dependent} configurations which associate a resource type to a * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} implementation - * + * * @return the list of {@link Dependent} configurations */ Dependent[] dependents() default {}; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java index e0c0060646..9543b7d582 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java @@ -22,7 +22,7 @@ public class ManagedDependentResourceContext { * Retrieve a contextual object, if it exists and is of the specified expected type, associated * with the specified key. Contextual objects can be used to pass data between the reconciler and * dependent resources and are scoped to the current reconciliation. - * + * * @param key the key identifying which contextual object to retrieve * @param expectedType the class representing the expected type of the contextual object * @param the type of the expected contextual object @@ -39,7 +39,7 @@ public Optional get(Object key, Class expectedType) { * Associates the specified contextual value to the specified key. If the value is {@code null}, * the semantics of this operation is defined as removing the mapping associated with the * specified key. - * + * * @param key the key identifying which contextual object to add or remove from the context * @param value the value to add to the context or {@code null} to remove an existing entry * associated with the specified key @@ -75,7 +75,7 @@ public ManagedDependentResourceContext(List dependentResource /** * Retrieve all the known {@link DependentResource} implementations - * + * * @return a list of known {@link DependentResource} implementations */ public List getDependentResources() { @@ -84,7 +84,7 @@ public List getDependentResources() { /** * Retrieve the dependent resource implementation associated with the specified resource type. - * + * * @param resourceClass the dependent resource class for which we want to retrieve the associated * dependent resource implementation * @param the type of the resources for which we want to retrieve the associated dependent 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 64c93484b8..fb14ea6847 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 @@ -28,7 +28,7 @@ * received from the informer, it means that the cache of the informer was updated, so it already * contains a more fresh version of the resource. *

- * + * * @param resource to cache. */ public class TemporaryResourceCache { From 9d269a62cb7ede1cfe96292fd613a405dc0ddb65 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 4 Mar 2022 09:40:52 +0100 Subject: [PATCH 0332/1608] feat: matcher avoiding creating the desired state when possible (#992) --- .../dependent/AbstractDependentResource.java | 14 +++-- .../api/reconciler/dependent/Matcher.java | 33 ++++++++++- .../api/reconciler/dependent/Updater.java | 16 ++++-- .../GenericKubernetesResourceMatcher.java | 41 ++++++++----- .../KubernetesDependentResource.java | 29 ++++++++-- .../GenericKubernetesResourceMatcherTest.java | 57 +++++++++++++------ .../dependent/SecretDependentResource.java | 18 +++--- 7 files changed, 152 insertions(+), 56 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 456e9339de..8700af0805 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -36,19 +36,23 @@ public void reconcile(P primary, Context context) { final var updatable = isUpdatable(primary, context); if (creatable || updatable) { var maybeActual = getResource(primary); - var desired = desired(primary, context); if (maybeActual.isEmpty()) { if (creatable) { + var desired = desired(primary, context); log.debug("Creating dependent {} for primary {}", desired, primary); creator.create(desired, primary, context); } } else { final var actual = maybeActual.get(); - if (updatable && !updater.match(actual, desired, context)) { - log.debug("Updating dependent {} for primary {}", desired, primary); - updater.update(actual, desired, primary, context); + if (updatable) { + final var match = updater.match(actual, primary, context); + if (!match.matched()) { + final var desired = match.computedDesired().orElse(desired(primary, context)); + log.debug("Updating dependent {} for primary {}", desired, primary); + updater.update(actual, desired, primary, context); + } } else { - log.debug("Update skipped for dependent {} as it matched the existing one", desired); + log.debug("Update skipped for dependent {} as it matched the existing one", actual); } } } else { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java index ad82daf661..f0c5bd7fd9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java @@ -1,7 +1,36 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -public interface Matcher { - boolean match(R actualResource, R desiredResource, Context context); +public interface Matcher { + interface Result { + boolean matched(); + + default Optional computedDesired() { + return Optional.empty(); + } + + static Result nonComputed(boolean matched) { + return () -> matched; + } + + static Result computed(boolean matched, T computedDesired) { + return new Result<>() { + @Override + public boolean matched() { + return matched; + } + + @Override + public Optional computedDesired() { + return Optional.of(computedDesired); + } + }; + } + } + + Result match(R actualResource, P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java index 6984f788f8..62199e1ede 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -1,18 +1,22 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; -import java.util.Objects; - import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; @SuppressWarnings("rawtypes") public interface Updater { - Updater NOOP = (actual, desired, primary, context) -> { + Updater NOOP = new Updater() { + @Override + public void update(Object actual, Object desired, HasMetadata primary, Context context) {} + + @Override + public Result match(Object actualResource, HasMetadata primary, Context context) { + return Result.nonComputed(true); + } }; void update(R actual, R desired, P primary, Context context); - default boolean match(R actualResource, R desiredResource, Context context) { - return Objects.equals(actualResource, desiredResource); - } + Result match(R actualResource, P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 1c3c7d0822..25d7c098d5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -8,37 +8,52 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; -public class GenericKubernetesResourceMatcher implements Matcher { +public class GenericKubernetesResourceMatcher + implements Matcher { - private GenericKubernetesResourceMatcher() {} + private final KubernetesDependentResource dependentResource; - @SuppressWarnings({"rawtypes", "unchecked"}) - public static Matcher matcherFor(Class resourceType) { + private GenericKubernetesResourceMatcher(KubernetesDependentResource dependentResource) { + this.dependentResource = dependentResource; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static Matcher matcherFor( + Class resourceType, KubernetesDependentResource dependentResource) { if (Secret.class.isAssignableFrom(resourceType)) { - return (actual, desired, context) -> ResourceComparators.compareSecretData((Secret) desired, - (Secret) actual); + return (actual, primary, context) -> { + final var desired = dependentResource.desired(primary, context); + return Result.computed( + ResourceComparators.compareSecretData((Secret) desired, (Secret) actual), desired); + }; } else if (ConfigMap.class.isAssignableFrom(resourceType)) { - return (actual, desired, context) -> ResourceComparators - .compareConfigMapData((ConfigMap) desired, (ConfigMap) actual); + return (actual, primary, context) -> { + final var desired = dependentResource.desired(primary, context); + return Result.computed( + ResourceComparators.compareConfigMapData((ConfigMap) desired, (ConfigMap) actual), + desired); + }; } else { - return new GenericKubernetesResourceMatcher(); + return new GenericKubernetesResourceMatcher(dependentResource); } } @Override - public boolean match(R actualResource, R desiredResource, Context context) { + public Result match(R actualResource, P primary, Context context) { final var objectMapper = context.getConfigurationService().getObjectMapper(); + final var desired = dependentResource.desired(primary, context); + // reflection will be replaced by this: // https://github.com/fabric8io/kubernetes-client/issues/3816 - var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desiredResource)); + var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desired)); var actualSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(actualResource)); var diffJsonPatch = JsonDiff.asJson(desiredSpecNode, actualSpecNode); for (int i = 0; i < diffJsonPatch.size(); i++) { String operation = diffJsonPatch.get(i).get("op").asText(); if (!operation.equals("add")) { - return false; + return Result.computed(false, desired); } } - return true; + return Result.computed(true, desired); } } 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 bbdf74da5c..04e3c87a54 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 @@ -21,7 +21,9 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -39,14 +41,24 @@ public abstract class KubernetesDependentResource informerEventSource; private boolean addOwnerReference; - private final Matcher matcher; + private final Matcher matcher; private final ResourceUpdatePreProcessor processor; @SuppressWarnings("unchecked") public KubernetesDependentResource() { - init(this::create, this::update, this::delete); - matcher = this instanceof Matcher ? (Matcher) this - : GenericKubernetesResourceMatcher.matcherFor(resourceType()); + init(this::create, new Updater<>() { + @Override + public void update(R actual, R desired, P primary, Context context) { + KubernetesDependentResource.this.update(actual, desired, primary, context); + } + + @Override + public Result match(R actualResource, P primary, Context context) { + return KubernetesDependentResource.this.match(actualResource, primary, context); + } + }, this::delete); + matcher = this instanceof Matcher ? (Matcher) this + : GenericKubernetesResourceMatcher.matcherFor(resourceType(), this); processor = this instanceof ResourceUpdatePreProcessor ? (ResourceUpdatePreProcessor) this : GenericResourceUpdatePreProcessor.processorFor(resourceType()); @@ -117,8 +129,8 @@ public void update(R actual, R target, P primary, Context context) { } } - public boolean match(R actualResource, R desiredResource, Context context) { - return matcher.match(actualResource, desiredResource, context); + public Result match(R actualResource, P primary, Context context) { + return matcher.match(actualResource, primary, context); } public void delete(P primary, Context context) { @@ -173,4 +185,9 @@ public Optional getResource(P primaryResource) { public void setKubernetesClient(KubernetesClient kubernetesClient) { this.client = kubernetesClient; } + + @Override + protected R desired(P primary, Context context) { + return super.desired(primary, context); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 6302c20e6d..3ae424cdfb 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -1,8 +1,12 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; +import java.util.Optional; + import org.junit.jupiter.api.Test; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -24,29 +28,40 @@ class GenericKubernetesResourceMatcherTest { @Test void checksIfDesiredValuesAreTheSame() { - var target1 = createDeployment(); - var desired1 = createDeployment(); - final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class); - assertThat(matcher.match(target1, desired1, context)).isTrue(); - - var target2 = createDeployment(); - var desired2 = createDeployment(); - target2.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); - assertThat(matcher.match(target2, desired2, context)) + var actual = createDeployment(); + final var desired = createDeployment(); + final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class, + new KubernetesDependentResource<>() { + @Override + protected Deployment desired(HasMetadata primary, Context context) { + final var currentCase = Optional.ofNullable(primary) + .map(p -> p.getMetadata().getLabels().get("case")) + .orElse(null); + var d = desired; + if ("removed".equals(currentCase)) { + d = createDeployment(); + d.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); + } + return d; + } + }); + assertThat(matcher.match(actual, null, context).matched()).isTrue(); + assertThat(matcher.match(actual, null, context).computedDesired().isPresent()).isTrue(); + assertThat(matcher.match(actual, null, context).computedDesired().get()).isEqualTo(desired); + + actual.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); + assertThat(matcher.match(actual, null, context).matched()) .withFailMessage("Additive changes should be ok") .isTrue(); - var target3 = createDeployment(); - var desired3 = createDeployment(); - desired3.getSpec().getTemplate().getMetadata().getLabels().put("new-key", "val"); - assertThat(matcher.match(target3, desired3, context)) + actual = createDeployment(); + assertThat(matcher.match(actual, createPrimary("removed"), context).matched()) .withFailMessage("Removed value should not be ok") .isFalse(); - var target4 = createDeployment(); - var desired4 = createDeployment(); - target4.getSpec().setReplicas(2); - assertThat(matcher.match(target4, desired4, context)) + actual = createDeployment(); + actual.getSpec().setReplicas(2); + assertThat(matcher.match(actual, null, context).matched()) .withFailMessage("Changed values are not ok") .isFalse(); } @@ -55,4 +70,12 @@ Deployment createDeployment() { return ReconcilerUtils.loadYaml( Deployment.class, GenericKubernetesResourceMatcherTest.class, "nginx-deployment.yaml"); } + + HasMetadata createPrimary(String caseName) { + return new DeploymentBuilder() + .editOrNewMetadata() + .addToLabels("case", caseName) + .endMetadata() + .build(); + } } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index b118b07f0c..6c71252bb5 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -8,6 +8,7 @@ import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; @@ -32,7 +33,7 @@ protected Secret desired(MySQLSchema schema, Context context) { final var password = RandomStringUtils .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here final var name = schema.getMetadata().getName(); - final var secretName = String.format(SECRET_FORMAT, name); + final var secretName = getSecretName(name); final var userName = String.format(USERNAME_FORMAT, name); return new SecretBuilder() @@ -40,16 +41,19 @@ protected Secret desired(MySQLSchema schema, Context context) { .withName(secretName) .withNamespace(schema.getMetadata().getNamespace()) .endMetadata() - .addToData( - MYSQL_SECRET_USERNAME, encode(userName)) - .addToData( - MYSQL_SECRET_PASSWORD, encode(password)) + .addToData(MYSQL_SECRET_USERNAME, encode(userName)) + .addToData(MYSQL_SECRET_PASSWORD, encode(password)) .build(); } + private String getSecretName(String name) { + return String.format(SECRET_FORMAT, name); + } + @Override - public boolean match(Secret actual, Secret target, Context context) { - return ResourceID.fromResource(actual).equals(ResourceID.fromResource(target)); + public Result match(Secret actual, MySQLSchema primary, Context context) { + final var desiredSecretName = getSecretName(primary.getMetadata().getName()); + return Result.nonComputed(actual.getMetadata().getName().equals(desiredSecretName)); } @Override From 45fb7e045c3ef5bf4f3bf391d23672e5baa181b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 4 Mar 2022 18:41:55 +0100 Subject: [PATCH 0333/1608] feat: Dependent Resources for External Resources (#991) --- .../dependent/AbstractDependentResource.java | 92 ++++++++++-- .../api/reconciler/dependent/Creator.java | 7 +- .../api/reconciler/dependent/Deleter.java | 5 +- .../dependent/DependentResource.java | 2 +- .../dependent/DesiredEqualsMatcher.java | 19 +++ .../dependent/EventSourceProvider.java | 7 +- .../dependent/RecentOperationCacheFiller.java | 10 ++ .../dependent/RecentOperationEventFilter.java | 11 ++ .../api/reconciler/dependent/Updater.java | 13 +- .../dependent/DependentResourceManager.java | 4 +- .../AbstractCachingDependentResource.java | 34 +++++ .../AbstractSimpleDependentResource.java | 73 ++++++++++ .../PerResourcePollingDependentResource.java | 40 +++++ .../external/PollingDependentResource.java | 36 +++++ .../KubernetesDependentResource.java | 47 ++---- .../processing/event/EventProcessor.java | 5 +- .../ExternalResourceCachingEventSource.java | 54 +++++++ .../event/source/CachingEventSource.java | 74 ++-------- .../event/source/ConcurrentHashMapCache.java | 38 +++++ .../inbound/CachingInboundEventSource.java | 5 +- .../source/informer/InformerEventSource.java | 24 ++- .../informer/ManagedInformerEventSource.java | 13 +- .../PerResourcePollingEventSource.java | 55 +++---- .../source/polling/PollingEventSource.java | 17 ++- .../javaoperatorsdk/operator/TestUtils.java | 4 + .../AbstractSimpleDependentResourceTest.java | 137 ++++++++++++++++++ .../KubernetesDependentResourceTest.java | 77 ---------- .../processing/event/EventProcessorTest.java | 3 +- ...ternalResourceCachingEventSourceTest.java} | 13 +- .../informer/InformerEventSourceTest.java | 38 +++-- .../PerResourcePollingEventSourceTest.java | 18 +-- .../sample/simple/TestCustomResource.java | 1 + ...CreateUpdateEventFilterTestReconciler.java | 17 ++- .../StandaloneDependentTestReconciler.java | 7 +- .../sample/MySQLSchemaReconciler.java | 2 +- .../dependent/SchemaDependentResource.java | 27 +--- ...java => SchemaPollingResourceFetcher.java} | 8 +- .../sample/MySQLSchemaOperatorE2E.java | 2 +- .../operator/sample/TomcatOperatorE2E.java | 4 +- .../WebPageReconcilerDependentResources.java | 11 +- 40 files changed, 720 insertions(+), 334 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ConcurrentHashMapCache.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/{CachingEventSourceTest.java => ExternalResourceCachingEventSourceTest.java} (81%) rename sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/{SchemaPollingResourceSupplier.java => SchemaPollingResourceFetcher.java} (70%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 8700af0805..af727b66eb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.event.ResourceID; public abstract class AbstractDependentResource implements DependentResource { @@ -13,21 +14,25 @@ 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; + private final boolean filteringEventSource; + private final boolean cachingEventSource; protected Creator creator; protected Updater updater; protected Deleter

deleter; @SuppressWarnings("unchecked") public AbstractDependentResource() { - init(Creator.NOOP, Updater.NOOP, Deleter.NOOP); - } - - @SuppressWarnings({"unchecked"}) - protected void init(Creator defaultCreator, Updater defaultUpdater, - Deleter

defaultDeleter) { - creator = creatable ? (Creator) this : defaultCreator; - updater = updatable ? (Updater) this : defaultUpdater; - deleter = deletable ? (Deleter

) this : defaultDeleter; + if (this instanceof EventSourceProvider) { + final var eventSource = ((EventSourceProvider

) this).getEventSource(); + filteringEventSource = eventSource instanceof RecentOperationEventFilter; + cachingEventSource = eventSource instanceof RecentOperationCacheFiller; + } else { + filteringEventSource = false; + cachingEventSource = false; + } + creator = creatable ? (Creator) this : null; + updater = updatable ? (Updater) this : null; + deleter = deletable ? (Deleter

) this : null; } @Override @@ -40,7 +45,7 @@ public void reconcile(P primary, Context context) { if (creatable) { var desired = desired(primary, context); log.debug("Creating dependent {} for primary {}", desired, primary); - creator.create(desired, primary, context); + handleCreate(desired, primary, context); } } else { final var actual = maybeActual.get(); @@ -49,7 +54,7 @@ public void reconcile(P primary, Context context) { if (!match.matched()) { final var desired = match.computedDesired().orElse(desired(primary, context)); log.debug("Updating dependent {} for primary {}", desired, primary); - updater.update(actual, desired, primary, context); + handleUpdate(actual, desired, primary, context); } } else { log.debug("Update skipped for dependent {} as it matched the existing one", actual); @@ -62,8 +67,71 @@ public void reconcile(P primary, Context context) { } } + protected void handleCreate(R desired, P primary, Context context) { + ResourceID resourceID = ResourceID.fromResource(primary); + R created = null; + try { + prepareEventFiltering(desired, resourceID); + created = creator.create(desired, primary, context); + cacheAfterCreate(resourceID, created); + } catch (RuntimeException e) { + cleanupAfterEventFiltering(desired, resourceID, created); + throw e; + } + } + + private void cleanupAfterEventFiltering(R desired, ResourceID resourceID, R created) { + if (filteringEventSource) { + eventSourceAsRecentOperationEventFilter() + .cleanupOnCreateOrUpdateEventFiltering(resourceID, created); + } + } + + private void cacheAfterCreate(ResourceID resourceID, R created) { + if (cachingEventSource) { + eventSourceAsRecentOperationCacheFiller().handleRecentResourceCreate(resourceID, created); + } + } + + private void cacheAfterUpdate(R actual, ResourceID resourceID, R updated) { + if (cachingEventSource) { + eventSourceAsRecentOperationCacheFiller().handleRecentResourceUpdate(resourceID, updated, + actual); + } + } + + private void prepareEventFiltering(R desired, ResourceID resourceID) { + if (filteringEventSource) { + eventSourceAsRecentOperationEventFilter().prepareForCreateOrUpdateEventFiltering(resourceID, + desired); + } + } + + protected void handleUpdate(R actual, R desired, P primary, Context context) { + ResourceID resourceID = ResourceID.fromResource(primary); + R updated = null; + try { + prepareEventFiltering(desired, resourceID); + updated = updater.update(actual, desired, primary, context); + cacheAfterUpdate(actual, resourceID, updated); + } catch (RuntimeException e) { + cleanupAfterEventFiltering(desired, resourceID, updated); + throw e; + } + } + + @SuppressWarnings("unchecked") + private RecentOperationEventFilter eventSourceAsRecentOperationEventFilter() { + return (RecentOperationEventFilter) ((EventSourceProvider

) this).getEventSource(); + } + + @SuppressWarnings("unchecked") + private RecentOperationCacheFiller eventSourceAsRecentOperationCacheFiller() { + return (RecentOperationCacheFiller) ((EventSourceProvider

) this).getEventSource(); + } + @Override - public void delete(P primary, Context context) { + public void cleanup(P primary, Context context) { if (isDeletable(primary, context)) { deleter.delete(primary, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java index f685a5edb4..223203b2b0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java @@ -3,10 +3,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -@SuppressWarnings("rawtypes") +@FunctionalInterface public interface Creator { - Creator NOOP = (desired, primary, context) -> { - }; - - void create(R desired, P primary, Context context); + R create(R desired, P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java index 7649460934..b81ffc0380 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java @@ -3,10 +3,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -@SuppressWarnings("rawtypes") +@FunctionalInterface public interface Deleter

{ - Deleter NOOP = (primary, context) -> { - }; - void delete(P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 09394f11b0..239ac58d0b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -8,7 +8,7 @@ public interface DependentResource { void reconcile(P primary, Context context); - default void delete(P primary, Context context) {} + default void cleanup(P primary, Context context) {} Optional getResource(P primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java new file mode 100644 index 0000000000..99470bf1d5 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; + +public class DesiredEqualsMatcher implements Matcher { + + private final AbstractDependentResource abstractDependentResource; + + public DesiredEqualsMatcher(AbstractDependentResource abstractDependentResource) { + this.abstractDependentResource = abstractDependentResource; + } + + @Override + public Result match(R actualResource, P primary, Context context) { + var desired = abstractDependentResource.desired(primary, context); + return Result.computed(actualResource.equals(desired), desired); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java index c9a2ca35a2..cc1514fac0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/EventSourceProvider.java @@ -5,6 +5,11 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; public interface EventSourceProvider

{ + /** + * @param context - event source context where the event source is initialized + * @return the initiated event source. + */ + EventSource initEventSource(EventSourceContext

context); - EventSource eventSource(EventSourceContext

context); + EventSource getEventSource(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java new file mode 100644 index 0000000000..7e27537d49 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationCacheFiller.java @@ -0,0 +1,10 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public interface RecentOperationCacheFiller { + + void handleRecentResourceCreate(ResourceID resourceID, R resource); + + void handleRecentResourceUpdate(ResourceID resourceID, R resource, R previousResourceVersion); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java new file mode 100644 index 0000000000..5bb9a13f5f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/RecentOperationEventFilter.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public interface RecentOperationEventFilter extends RecentOperationCacheFiller { + + void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID, R resource); + + void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID, R resource); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java index 62199e1ede..25e0567781 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -4,19 +4,8 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; -@SuppressWarnings("rawtypes") public interface Updater { - Updater NOOP = new Updater() { - @Override - public void update(Object actual, Object desired, HasMetadata primary, Context context) {} - - @Override - public Result match(Object actualResource, HasMetadata primary, Context context) { - return Result.nonComputed(true); - } - }; - - void update(R actual, R desired, P primary, Context context); + R update(R actual, R desired, P primary, Context context); Result match(R actualResource, P primary, Context context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 3f46e12ca5..554f019d81 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -54,7 +54,7 @@ public List prepareEventSources(EventSourceContext

context) { final var dependentResource = createAndConfigureFrom(drc, context.getClient()); if (dependentResource instanceof EventSourceProvider) { EventSourceProvider provider = (EventSourceProvider) dependentResource; - sources.add(provider.eventSource(context)); + sources.add(provider.initEventSource(context)); } return dependentResource; }) @@ -72,7 +72,7 @@ public UpdateControl

reconcile(P resource, Context context) { @Override public DeleteControl cleanup(P resource, Context context) { initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.delete(resource, context)); + dependents.forEach(dependent -> dependent.cleanup(resource, context)); return Reconciler.super.cleanup(resource, context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java new file mode 100644 index 0000000000..86f2c23d54 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.Utils; +import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +public abstract class AbstractCachingDependentResource + extends AbstractDependentResource implements EventSourceProvider

{ + + protected ExternalResourceCachingEventSource eventSource; + + public Optional fetchResource(P primaryResource) { + return eventSource.getAssociated(primaryResource); + } + + @Override + public EventSource getEventSource() { + return eventSource; + } + + protected Class resourceType() { + return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); + } + + @Override + public Optional getResource(P primaryResource) { + return eventSource.getAssociated(primaryResource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java new file mode 100644 index 0000000000..18de3390e0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -0,0 +1,73 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DesiredEqualsMatcher; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ConcurrentHashMapCache; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; + +/** A base class for external dependent resources that don't have an event source. */ +public abstract class AbstractSimpleDependentResource + extends AbstractDependentResource { + + // cache serves only to keep the resource readable again until next reconciliation when the + // new resource is read again. + protected final UpdatableCache cache; + protected Matcher matcher; + + public AbstractSimpleDependentResource() { + this(new ConcurrentHashMapCache<>()); + } + + public AbstractSimpleDependentResource(UpdatableCache cache) { + this.cache = cache; + initMatcher(); + } + + @Override + public Optional getResource(HasMetadata primaryResource) { + return cache.get(ResourceID.fromResource(primaryResource)); + } + + /** Actually read the resource from the target API */ + public abstract Optional fetchResource(HasMetadata primaryResource); + + @Override + public void reconcile(P primary, Context context) { + var resourceId = ResourceID.fromResource(primary); + Optional resource = fetchResource(primary); + resource.ifPresentOrElse(r -> cache.put(resourceId, r), () -> cache.remove(resourceId)); + super.reconcile(primary, context); + } + + public void cleanup(P primary, Context context) { + super.cleanup(primary, context); + cache.remove(ResourceID.fromResource(primary)); + } + + @Override + protected void handleCreate(R desired, P primary, Context context) { + var res = this.creator.create(desired, primary, context); + cache.put(ResourceID.fromResource(primary), res); + } + + @Override + protected void handleUpdate(R actual, R desired, P primary, Context context) { + var res = updater.update(actual, desired, primary, context); + cache.put(ResourceID.fromResource(primary), res); + } + + public Matcher.Result match(R actualResource, P primary, Context context) { + return matcher.match(actualResource, primary, context); + } + + protected void initMatcher() { + matcher = new DesiredEqualsMatcher<>(this); + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java new file mode 100644 index 0000000000..c5f9e2c888 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java @@ -0,0 +1,40 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; + +import static io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource.DEFAULT_POLLING_PERIOD; + +public abstract class PerResourcePollingDependentResource + extends AbstractCachingDependentResource + implements PerResourcePollingEventSource.ResourceFetcher { + + protected long pollingPeriod; + + public PerResourcePollingDependentResource() { + this(DEFAULT_POLLING_PERIOD); + } + + public PerResourcePollingDependentResource(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + eventSource = new PerResourcePollingEventSource<>(this, context.getPrimaryCache(), + pollingPeriod, resourceType()); + return eventSource; + } + + public PerResourcePollingDependentResource setPollingPeriod(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + return this; + } + + public long getPollingPeriod() { + return pollingPeriod; + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java new file mode 100644 index 0000000000..18e0c66b60 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java @@ -0,0 +1,36 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import java.util.Map; +import java.util.function.Supplier; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.polling.PollingEventSource; + +public abstract class PollingDependentResource + extends AbstractCachingDependentResource implements Supplier> { + + public static final int DEFAULT_POLLING_PERIOD = 5000; + protected long pollingPeriod; + + public PollingDependentResource() { + this(DEFAULT_POLLING_PERIOD); + } + + public PollingDependentResource(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + } + + @Override + public EventSource initEventSource(EventSourceContext

context) { + eventSource = new PollingEventSource<>(this, pollingPeriod, resourceType()); + return eventSource; + } + + public PollingDependentResource setPollingPeriod(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + return this; + } +} 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 04e3c87a54..c94cec73d8 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 @@ -23,7 +23,6 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -46,19 +45,9 @@ public abstract class KubernetesDependentResource() { - @Override - public void update(R actual, R desired, P primary, Context context) { - KubernetesDependentResource.this.update(actual, desired, primary, context); - } - - @Override - public Result match(R actualResource, P primary, Context context) { - return KubernetesDependentResource.this.match(actualResource, primary, context); - } - }, this::delete); matcher = this instanceof Matcher ? (Matcher) this : GenericKubernetesResourceMatcher.matcherFor(resourceType(), this); + processor = this instanceof ResourceUpdatePreProcessor ? (ResourceUpdatePreProcessor) this : GenericResourceUpdatePreProcessor.processorFor(resourceType()); @@ -103,30 +92,13 @@ public void configureWith( this.addOwnerReference = addOwnerReference; } - public void create(R target, P primary, Context context) { - var resourceID = ResourceID.fromResource(target); - try { - informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID); - var created = prepare(target, primary, "Creating").create(target); - informerEventSource.handleRecentResourceCreate(created); - } catch (RuntimeException e) { - informerEventSource.cleanupOnCreateOrUpdateEventFiltering(resourceID); - throw e; - } + public R create(R target, P primary, Context context) { + return prepare(target, primary, "Creating").create(target); } - public void update(R actual, R target, P primary, Context context) { - var resourceID = ResourceID.fromResource(target); - try { - var updatedActual = processor.replaceSpecOnActual(actual, target, context); - informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID); - var updated = prepare(target, primary, "Updating").replace(updatedActual); - informerEventSource.handleRecentResourceUpdate(updated, - actual.getMetadata().getResourceVersion()); - } catch (RuntimeException e) { - informerEventSource.cleanupOnCreateOrUpdateEventFiltering(resourceID); - throw e; - } + public R update(R actual, R target, P primary, Context context) { + var updatedActual = processor.replaceSpecOnActual(actual, target, context); + return prepare(target, primary, "Updating").replace(updatedActual); } public Result match(R actualResource, P primary, Context context) { @@ -155,7 +127,7 @@ protected NonNamespaceOperation, Resource> prepa } @Override - public EventSource eventSource(EventSourceContext

context) { + public EventSource initEventSource(EventSourceContext

context) { if (informerEventSource == null) { configureWith(context.getConfigurationService(), null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); @@ -190,4 +162,9 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { protected R desired(P primary, Context context) { return super.desired(primary, context); } + + @Override + public EventSource getEventSource() { + return informerEventSource; + } } 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 81e637a2f6..9ea6df7332 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 @@ -208,8 +208,9 @@ void eventProcessingFinished( cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { postExecutionControl.getUpdatedCustomResource().ifPresent(r -> { - eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate(r, - executionScope.getResource().getMetadata().getResourceVersion()); + eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate( + ResourceID.fromResource(r), r, + executionScope.getResource()); }); if (eventMarker.eventPresent(resourceID)) { submitReconciliationExecution(resourceID); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java new file mode 100644 index 0000000000..5ffa9b2166 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator.processing.event; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; +import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; + +public class ExternalResourceCachingEventSource + extends CachingEventSource + implements RecentOperationCacheFiller { + + public ExternalResourceCachingEventSource(Class resourceClass) { + super(resourceClass); + } + + public synchronized void handleDelete(ResourceID relatedResourceID) { + if (!isRunning()) { + return; + } + var cachedValue = cache.get(relatedResourceID); + cache.remove(relatedResourceID); + // we only propagate event if the resource was previously in cache + if (cachedValue.isPresent()) { + getEventHandler().handleEvent(new Event(relatedResourceID)); + } + } + + public synchronized void handleEvent(R value, ResourceID relatedResourceID) { + if (!isRunning()) { + return; + } + var cachedValue = cache.get(relatedResourceID); + if (cachedValue.map(v -> !v.equals(value)).orElse(true)) { + cache.put(relatedResourceID, value); + getEventHandler().handleEvent(new Event(relatedResourceID)); + } + } + + @Override + public synchronized void handleRecentResourceCreate(ResourceID resourceID, R resource) { + if (cache.get(resourceID).isEmpty()) { + cache.put(resourceID, resource); + } + } + + @Override + public synchronized void handleRecentResourceUpdate(ResourceID resourceID, R resource, + R previousResourceVersion) { + cache.get(resourceID).ifPresent(r -> { + if (r.equals(previousResourceVersion)) { + cache.put(resourceID, resource); + } + }); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index aac04115a3..cfc065331d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -1,13 +1,10 @@ package io.javaoperatorsdk.operator.processing.event.source; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.ResourceID; /** @@ -20,20 +17,20 @@ * an event if the resource is new or not equals to the one in the cache, and if accepted by the * filter if one is present. * - * @param represents the type of resources (usually external non-kubernetes ones) being handled. + * @param represents the type of resources (usually external non-kubernetes ones) being handled. */ -public abstract class CachingEventSource - extends AbstractResourceEventSource implements Cache { +public abstract class CachingEventSource + extends AbstractResourceEventSource implements Cache { - protected UpdatableCache cache; + protected UpdatableCache cache; - protected CachingEventSource(Class resourceClass) { + protected CachingEventSource(Class resourceClass) { super(resourceClass); cache = initCache(); } @Override - public Optional get(ResourceID resourceID) { + public Optional get(ResourceID resourceID) { return cache.get(resourceID); } @@ -48,72 +45,23 @@ public Stream keys() { } @Override - public Stream list(Predicate predicate) { + public Stream list(Predicate predicate) { return cache.list(predicate); } - protected void handleDelete(ResourceID relatedResourceID) { - if (!isRunning()) { - return; - } - var cachedValue = cache.get(relatedResourceID); - cache.remove(relatedResourceID); - // we only propagate event if the resource was previously in cache - if (cachedValue.isPresent()) { - getEventHandler().handleEvent(new Event(relatedResourceID)); - } - } - protected void handleEvent(T value, ResourceID relatedResourceID) { - if (!isRunning()) { - return; - } - var cachedValue = cache.get(relatedResourceID); - if (cachedValue.map(v -> !v.equals(value)).orElse(true)) { - cache.put(relatedResourceID, value); - getEventHandler().handleEvent(new Event(relatedResourceID)); - } - } - protected UpdatableCache initCache() { - return new MapCache<>(); + protected UpdatableCache initCache() { + return new ConcurrentHashMapCache<>(); } - public Optional getCachedValue(ResourceID resourceID) { + public Optional getCachedValue(ResourceID resourceID) { return cache.get(resourceID); } @Override - public Optional getAssociated(P primary) { + public Optional getAssociated(P primary) { return cache.get(ResourceID.fromResource(primary)); } - protected static class MapCache implements UpdatableCache { - private final Map cache = new ConcurrentHashMap<>(); - - @Override - public Optional get(ResourceID resourceID) { - return Optional.ofNullable(cache.get(resourceID)); - } - - @Override - public Stream keys() { - return cache.keySet().stream(); - } - - @Override - public Stream list(Predicate predicate) { - return cache.values().stream().filter(predicate); - } - - @Override - public T remove(ResourceID key) { - return cache.remove(key); - } - - @Override - public void put(ResourceID key, T resource) { - cache.put(key, resource); - } - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ConcurrentHashMapCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ConcurrentHashMapCache.java new file mode 100644 index 0000000000..76dc615fcb --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ConcurrentHashMapCache.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.processing.event.source; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class ConcurrentHashMapCache implements UpdatableCache { + private final Map cache = new ConcurrentHashMap<>(); + + @Override + public Optional get(ResourceID resourceID) { + return Optional.ofNullable(cache.get(resourceID)); + } + + @Override + public Stream keys() { + return cache.keySet().stream(); + } + + @Override + public Stream list(Predicate predicate) { + return cache.values().stream().filter(predicate); + } + + @Override + public T remove(ResourceID key) { + return cache.remove(key); + } + + @Override + public void put(ResourceID key, T resource) { + cache.put(key, resource); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java index 1d60da2a3a..34f37a048b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java @@ -1,10 +1,11 @@ package io.javaoperatorsdk.operator.processing.event.source.inbound; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; -public class CachingInboundEventSource extends CachingEventSource { +public class CachingInboundEventSource + extends ExternalResourceCachingEventSource { public CachingInboundEventSource(Class resourceClass) { super(resourceClass); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 006564426e..4a23bd9363 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -10,6 +10,7 @@ import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationEventFilter; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -63,7 +64,7 @@ */ public class InformerEventSource extends ManagedInformerEventSource> - implements ResourceCache, ResourceEventHandler { + implements ResourceCache, ResourceEventHandler, RecentOperationEventFilter { private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); @@ -160,14 +161,17 @@ public InformerConfiguration getConfiguration() { } @Override - public void handleRecentResourceUpdate(R resource, String previousResourceVersion) { + public void handleRecentResourceUpdate(ResourceID resourceID, R resource, + R previousResourceVersion) { handleRecentCreateOrUpdate(resource, - () -> super.handleRecentResourceUpdate(resource, previousResourceVersion)); + () -> super.handleRecentResourceUpdate(resourceID, resource, + previousResourceVersion)); } @Override - public void handleRecentResourceCreate(R resource) { - handleRecentCreateOrUpdate(resource, () -> super.handleRecentResourceCreate(resource)); + public void handleRecentResourceCreate(ResourceID resourceID, R resource) { + handleRecentCreateOrUpdate(resource, + () -> super.handleRecentResourceCreate(resourceID, resource)); } private synchronized void handleRecentCreateOrUpdate(R resource, Runnable runnable) { @@ -216,7 +220,9 @@ private synchronized void handleRecentResourceOperationAndStopEventRecording(R r } } - public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID) { + @Override + public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID, + R resource) { log.info("Starting event recording for: {}", resourceID); eventRecorder.startEventRecording(resourceID); } @@ -225,9 +231,11 @@ public synchronized void prepareForCreateOrUpdateEventFiltering(ResourceID resou * Mean to be called to clean up in case of an exception from the client. Usually in a catch * block. * - * @param resourceID of the resource + * @param resource handled by the informer */ - public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID) { + @Override + public synchronized void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID, + R resource) { log.info("Stopping event recording for: {}", resourceID); eventRecorder.stopEventRecording(resourceID); } 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 47de996914..063207b0e7 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 @@ -13,6 +13,7 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; @@ -20,7 +21,7 @@ public abstract class ManagedInformerEventSource> extends CachingEventSource - implements ResourceEventHandler, ResourceCache { + implements ResourceEventHandler, ResourceCache, RecentOperationCacheFiller { private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); @@ -68,11 +69,15 @@ public void stop() { manager().stop(); } - public void handleRecentResourceUpdate(R resource, String previousResourceVersion) { - temporaryResourceCache.putUpdatedResource(resource, previousResourceVersion); + @Override + public void handleRecentResourceUpdate(ResourceID resourceID, R resource, + R previousResourceVersion) { + temporaryResourceCache.putUpdatedResource(resource, + previousResourceVersion.getMetadata().getResourceVersion()); } - public void handleRecentResourceCreate(R resource) { + @Override + public void handleRecentResourceCreate(ResourceID resourceID, R resource) { temporaryResourceCache.putAddedResource(resource); } 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 9130d27a45..74cf420cd5 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 @@ -12,6 +12,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.Cache; import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; @@ -25,39 +26,39 @@ *

* For other behavior see {@link CachingEventSource} * - * @param the resource polled by the event source - * @param related custom resource + * @param the resource polled by the event source + * @param

related custom resource */ -public class PerResourcePollingEventSource - extends CachingEventSource - implements ResourceEventAware { +public class PerResourcePollingEventSource + extends ExternalResourceCachingEventSource + implements ResourceEventAware

{ private static final Logger log = LoggerFactory.getLogger(PerResourcePollingEventSource.class); private final Timer timer = new Timer(); private final Map timerTasks = new ConcurrentHashMap<>(); - private final ResourceSupplier resourceSupplier; - private final Cache resourceCache; - private final Predicate registerPredicate; + private final ResourceFetcher resourceFetcher; + private final Cache

resourceCache; + private final Predicate

registerPredicate; private final long period; - public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - Cache resourceCache, long period, Class resourceClass) { - this(resourceSupplier, resourceCache, period, null, resourceClass); + public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, + Cache

resourceCache, long period, Class resourceClass) { + this(resourceFetcher, resourceCache, period, null, resourceClass); } - public PerResourcePollingEventSource(ResourceSupplier resourceSupplier, - Cache resourceCache, long period, - Predicate registerPredicate, Class resourceClass) { + public PerResourcePollingEventSource(ResourceFetcher resourceFetcher, + Cache

resourceCache, long period, + Predicate

registerPredicate, Class resourceClass) { super(resourceClass); - this.resourceSupplier = resourceSupplier; + this.resourceFetcher = resourceFetcher; this.resourceCache = resourceCache; this.period = period; this.registerPredicate = registerPredicate; } - private void pollForResource(R resource) { - var value = resourceSupplier.getResource(resource); + private void pollForResource(P resource) { + var value = resourceFetcher.fetchResource(resource); var resourceID = ResourceID.fromResource(resource); if (value.isEmpty()) { super.handleDelete(resourceID); @@ -66,10 +67,10 @@ private void pollForResource(R resource) { } } - private Optional getAndCacheResource(ResourceID resourceID) { + private Optional getAndCacheResource(ResourceID resourceID) { var resource = resourceCache.get(resourceID); if (resource.isPresent()) { - var value = resourceSupplier.getResource(resource.get()); + var value = resourceFetcher.fetchResource(resource.get()); value.ifPresent(v -> cache.put(resourceID, v)); return value; } @@ -77,17 +78,17 @@ private Optional getAndCacheResource(ResourceID resourceID) { } @Override - public void onResourceCreated(R resource) { + public void onResourceCreated(P resource) { checkAndRegisterTask(resource); } @Override - public void onResourceUpdated(R newResource, R oldResource) { + public void onResourceUpdated(P newResource, P oldResource) { checkAndRegisterTask(newResource); } @Override - public void onResourceDeleted(R resource) { + public void onResourceDeleted(P resource) { var resourceID = ResourceID.fromResource(resource); TimerTask task = timerTasks.remove(resourceID); if (task != null) { @@ -101,7 +102,7 @@ public void onResourceDeleted(R resource) { // since events from ResourceEventAware are propagated from the thread of the informer. This is // important // because otherwise there will be a race condition related to the timerTasks. - private void checkAndRegisterTask(R resource) { + private void checkAndRegisterTask(P resource) { var resourceID = ResourceID.fromResource(resource); if (timerTasks.get(resourceID) == null && (registerPredicate == null || registerPredicate.test(resource))) { @@ -131,7 +132,7 @@ public void run() { * @return the related resource for this event source */ @Override - public Optional getAssociated(R primary) { + public Optional getAssociated(P primary) { return getValueFromCacheOrSupplier(ResourceID.fromResource(primary)); } @@ -142,7 +143,7 @@ public Optional getAssociated(R primary) { * supplier. The value provided from the supplier is cached, but no new event is * propagated. */ - public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { + public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { var cachedValue = getCachedValue(resourceID); if (cachedValue.isPresent()) { return cachedValue; @@ -151,8 +152,8 @@ public Optional getValueFromCacheOrSupplier(ResourceID resourceID) { } } - public interface ResourceSupplier { - Optional getResource(R resource); + public interface ResourceFetcher { + Optional fetchResource(P primaryResource); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PollingEventSource.java index f9a4265c04..8a09b71658 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 @@ -8,8 +8,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; /** * Polls resource (on contrary to {@link PerResourcePollingEventSource}) not per resource bases but @@ -34,19 +34,20 @@ * cache. * * - * @param type of the polled resource + * @param type of the polled resource * @param

primary resource type */ -public class PollingEventSource extends CachingEventSource { +public class PollingEventSource + extends ExternalResourceCachingEventSource { private static final Logger log = LoggerFactory.getLogger(PollingEventSource.class); private final Timer timer = new Timer(); - private final Supplier> supplierToPoll; + private final Supplier> supplierToPoll; private final long period; - public PollingEventSource( - Supplier> supplier, long period, Class resourceClass) { + public PollingEventSource(Supplier> supplier, + long period, Class resourceClass) { super(resourceClass); this.supplierToPoll = supplier; this.period = period; @@ -77,7 +78,7 @@ protected void getStateAndFillCache() { cache.keys().filter(e -> !values.containsKey(e)).forEach(super::handleDelete); } - public void put(ResourceID key, T resource) { + public void put(ResourceID key, R resource) { cache.put(key, resource); } @@ -94,7 +95,7 @@ public void stop() throws OperatorException { * @return related resource */ @Override - public Optional getAssociated(P primary) { + public Optional getAssociated(P primary) { return getCachedValue(ResourceID.fromResource(primary)); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java index 137ed2d11d..e8361c49cf 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/TestUtils.java @@ -27,6 +27,10 @@ public static CustomResourceDefinition testCRD(String scope) { .build(); } + public static TestCustomResource testCustomResource1() { + return testCustomResource(new ResourceID("test1", "default")); + } + public static TestCustomResource testCustomResource(ResourceID id) { TestCustomResource resource = new TestCustomResource(); resource.setMetadata( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java new file mode 100644 index 0000000000..00557e7e61 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java @@ -0,0 +1,137 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import java.util.Optional; +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.TestUtils; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; +import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@SuppressWarnings("unchecked") +class AbstractSimpleDependentResourceTest { + + UpdatableCache updatableCacheMock = mock(UpdatableCache.class); + Supplier supplierMock = mock(Supplier.class); + + SimpleDependentResource simpleDependentResource = + new SimpleDependentResource(updatableCacheMock, supplierMock); + + @BeforeEach + void setup() { + when(supplierMock.get()).thenReturn(SampleExternalResource.testResource1()); + } + + @Test + void getsTheResourceFromSupplyIfReconciling() { + simpleDependentResource = new SimpleDependentResource(supplierMock); + + simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); + + verify(supplierMock, times(1)).get(); + assertThat(simpleDependentResource.getResource(TestUtils.testCustomResource1())) + .isPresent() + .isEqualTo(Optional.of(SampleExternalResource.testResource1())); + } + + @Test + void getResourceReadsTheResourceFromCache() { + simpleDependentResource.getResource(TestUtils.testCustomResource1()); + + verify(supplierMock, times(0)).get(); + verify(updatableCacheMock, times(1)).get(any()); + } + + @Test + void createPutsNewResourceToTheCache() { + when(supplierMock.get()).thenReturn(null); + when(updatableCacheMock.get(any())).thenReturn(Optional.empty()); + + simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); + + verify(updatableCacheMock, times(1)).put(any(), any()); + } + + @Test + void updatePutsNewResourceToCache() { + var actual = SampleExternalResource.testResource1(); + actual.setValue("changedValue"); + when(supplierMock.get()).thenReturn(actual); + when(updatableCacheMock.get(any())).thenReturn(Optional.of(actual)); + + simpleDependentResource.reconcile(TestUtils.testCustomResource1(), null); + + verify(updatableCacheMock, times(1)) + .put(ResourceID.fromResource(TestUtils.testCustomResource1()), actual); + + verify(updatableCacheMock, times(1)) + .put( + ResourceID.fromResource(TestUtils.testCustomResource1()), + SampleExternalResource.testResource1()); + } + + @Test + void deleteRemovesResourceFromCache() { + simpleDependentResource.cleanup(TestUtils.testCustomResource1(), null); + verify(updatableCacheMock, times(1)).remove(any()); + } + + private static class SimpleDependentResource + extends AbstractSimpleDependentResource + implements Creator, + Updater, + Deleter { + + private final Supplier supplier; + + public SimpleDependentResource(Supplier supplier) { + this.supplier = supplier; + } + + public SimpleDependentResource( + UpdatableCache cache, Supplier supplier) { + super(cache); + this.supplier = supplier; + } + + @Override + public Optional fetchResource(HasMetadata primaryResource) { + return Optional.ofNullable(supplier.get()); + } + + @Override + public SampleExternalResource create( + SampleExternalResource desired, TestCustomResource primary, Context context) { + return SampleExternalResource.testResource1(); + } + + @Override + public SampleExternalResource update( + SampleExternalResource actual, + SampleExternalResource desired, + TestCustomResource primary, + Context context) { + return SampleExternalResource.testResource1(); + } + + @Override + public void delete(TestCustomResource primary, Context context) {} + + @Override + protected SampleExternalResource desired(TestCustomResource primary, Context context) { + return SampleExternalResource.testResource1(); + } + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java deleted file mode 100644 index e73244f7c6..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent.kubernetes; - -import static org.mockito.Mockito.*; - -class KubernetesDependentResourceTest { - - // private InformerEventSource informerEventSourceMock = mock(InformerEventSource.class); - // private AssociatedSecondaryResourceIdentifier associatedResourceIdentifierMock = - // mock(AssociatedSecondaryResourceIdentifier.class); - // private ResourceMatcher resourceMatcherMock = mock(ResourceMatcher.class); - // private KubernetesDependentResource.ClientFacade clientFacadeMock = - // mock(KubernetesDependentResource.ClientFacade.class); - // - // KubernetesDependentResource kubernetesDependentResource = - // new KubernetesDependentResource() { - // { - // this.informerEventSource = informerEventSourceMock; - // this.resourceMatcher = resourceMatcherMock; - // this.clientFacade = clientFacadeMock; - // this.resourceUpdatePreProcessor = mock(ResourceUpdatePreProcessor.class); - // } - // - // @Override - // protected Object desired(HasMetadata primary, Context context) { - // return testResource(); - // } - // }; - // - // @BeforeEach - // public void setup() { - // InformerConfiguration informerConfigurationMock = mock(InformerConfiguration.class); - // when(informerEventSourceMock.getConfiguration()).thenReturn(informerConfigurationMock); - // when(informerConfigurationMock.getAssociatedResourceIdentifier()) - // .thenReturn(associatedResourceIdentifierMock); - // when(associatedResourceIdentifierMock.associatedSecondaryID(any())) - // .thenReturn(ResourceID.fromResource(testResource())); - // } - // - // @Test - // void updateCallsInformerJustUpdatedHandler() { - // when(resourceMatcherMock.match(any(), any(), any())).thenReturn(false); - // when(clientFacadeMock.replaceResource(any(), any(), any())).thenReturn(testResource()); - // when(informerEventSourceMock.getAssociated(any())).thenReturn(Optional.of(testResource())); - // - // kubernetesDependentResource.reconcile(primaryResource(), null); - // - // verify(informerEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any()); - // } - // - // @Test - // void createCallsInformerJustUpdatedHandler() { - // when(clientFacadeMock.createResource(any(), any(), any())).thenReturn(testResource()); - // when(informerEventSourceMock.getAssociated(any())).thenReturn(Optional.empty()); - // - // kubernetesDependentResource.reconcile(primaryResource(), null); - // - // verify(informerEventSourceMock, times(1)).handleRecentResourceAdd(any()); - // } - // - // TestCustomResource primaryResource() { - // TestCustomResource testCustomResource = new TestCustomResource(); - // testCustomResource.setMetadata(new ObjectMeta()); - // testCustomResource.getMetadata().setName("test"); - // testCustomResource.getMetadata().setNamespace("default"); - // return testCustomResource; - // } - // - // ConfigMap testResource() { - // ConfigMap configMap = new ConfigMap(); - // configMap.setMetadata(new ObjectMeta()); - // configMap.getMetadata().setName("test"); - // configMap.getMetadata().setNamespace("default"); - // configMap.getMetadata().setResourceVersion("0"); - // return configMap; - // } - -} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/EventProcessorTest.java index c02a3b0da1..a716a49898 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 @@ -247,7 +247,8 @@ void updatesEventSourceHandlerIfResourceUpdated() { eventProcessorWithRetry.eventProcessingFinished(executionScope, postExecutionControl); - verify(controllerResourceEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any()); + verify(controllerResourceEventSourceMock, times(1)).handleRecentResourceUpdate(any(), any(), + any()); } private ResourceID eventAlreadyUnderProcessing() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java similarity index 81% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java index 29cf0a981d..9ed4e25342 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java @@ -6,17 +6,18 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.Event; import io.javaoperatorsdk.operator.processing.event.EventHandler; +import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import static io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource.*; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; -class CachingEventSourceTest extends - AbstractEventSourceTestBase, EventHandler> { +class ExternalResourceCachingEventSourceTest extends + AbstractEventSourceTestBase, EventHandler> { @BeforeEach public void setup() { - setUpSource(new SimpleCachingEventSource()); + setUpSource(new SimpleExternalCachingEventSource()); } @Test @@ -65,9 +66,9 @@ public void noEventOnDeleteIfResourceWasNotInCacheBefore() { } - public static class SimpleCachingEventSource - extends CachingEventSource { - public SimpleCachingEventSource() { + public static class SimpleExternalCachingEventSource + extends ExternalResourceCachingEventSource { + public SimpleExternalCachingEventSource() { super(SampleExternalResource.class); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 1c901034e0..4caf9b70d2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -96,9 +96,11 @@ void notPropagatesEventIfAfterUpdateReceivedJustTheRelatedEvent() { informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onUpdate(prevTestDeployment, testDeployment); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); @@ -110,9 +112,11 @@ void notPropagatesEventIfAfterCreateReceivedJustTheRelatedEvent() { var testDeployment = testDeployment(); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onAdd(testDeployment); - informerEventSource.handleRecentResourceCreate(testDeployment); + informerEventSource.handleRecentResourceCreate(ResourceID.fromResource(testDeployment), + testDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); @@ -127,10 +131,12 @@ void propagatesEventIfNewEventReceivedAfterTheCurrentTargetEvent() { nextTestDeployment.getMetadata().setResourceVersion(NEXT_RESOURCE_VERSION); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onUpdate(prevTestDeployment, testDeployment); informerEventSource.onUpdate(testDeployment, nextTestDeployment); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(1)).handleEvent(any()); verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); @@ -145,10 +151,12 @@ void notPropagatesEventIfMoreReceivedButTheLastIsTheUpdated() { prevPrevTestDeployment.getMetadata().setResourceVersion("-1"); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment); informerEventSource.onUpdate(prevTestDeployment, testDeployment); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(0)).unconditionallyCacheResource(any()); @@ -157,10 +165,14 @@ void notPropagatesEventIfMoreReceivedButTheLastIsTheUpdated() { @Test void putsResourceOnTempCacheIfNoEventRecorded() { var testDeployment = testDeployment(); + var prevTestDeployment = testDeployment(); + prevTestDeployment.getMetadata().setResourceVersion(PREV_RESOURCE_VERSION); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any()); @@ -175,9 +187,11 @@ void putsResourceOnTempCacheIfNoEventRecordedWithSameResourceVersion() { prevPrevTestDeployment.getMetadata().setResourceVersion("-1"); informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment)); + .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(testDeployment), + testDeployment); informerEventSource.onUpdate(prevPrevTestDeployment, prevTestDeployment); - informerEventSource.handleRecentResourceUpdate(testDeployment, PREV_RESOURCE_VERSION); + informerEventSource.handleRecentResourceUpdate(ResourceID.fromResource(testDeployment), + testDeployment, prevTestDeployment); verify(eventHandlerMock, times(0)).handleEvent(any()); verify(temporaryResourceCacheMock, times(1)).unconditionallyCacheResource(any()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java index 8486f705ca..eb8d80c0c1 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSourceTest.java @@ -27,15 +27,15 @@ class PerResourcePollingEventSourceTest extends AbstractEventSourceTestBase, EventHandler> { public static final int PERIOD = 80; - private PerResourcePollingEventSource.ResourceSupplier supplier = - mock(PerResourcePollingEventSource.ResourceSupplier.class); + private PerResourcePollingEventSource.ResourceFetcher supplier = + mock(PerResourcePollingEventSource.ResourceFetcher.class); private Cache resourceCache = mock(Cache.class); private TestCustomResource testCustomResource = TestUtils.testCustomResource(); @BeforeEach public void setup() { when(resourceCache.get(any())).thenReturn(Optional.of(testCustomResource)); - when(supplier.getResource(any())) + when(supplier.fetchResource(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())); setUpSource(new PerResourcePollingEventSource<>(supplier, resourceCache, PERIOD, @@ -47,7 +47,7 @@ public void pollsTheResourceAfterAwareOfIt() throws InterruptedException { source.onResourceCreated(testCustomResource); Thread.sleep(3 * PERIOD); - verify(supplier, atLeast(2)).getResource(eq(testCustomResource)); + verify(supplier, atLeast(2)).fetchResource(eq(testCustomResource)); verify(eventHandler, times(1)).handleEvent(any()); } @@ -59,31 +59,31 @@ public void registeringTaskOnAPredicate() throws InterruptedException { source.onResourceCreated(testCustomResource); Thread.sleep(2 * PERIOD); - verify(supplier, times(0)).getResource(eq(testCustomResource)); + verify(supplier, times(0)).fetchResource(eq(testCustomResource)); testCustomResource.getMetadata().setGeneration(2L); source.onResourceUpdated(testCustomResource, testCustomResource); Thread.sleep(2 * PERIOD); - verify(supplier, atLeast(1)).getResource(eq(testCustomResource)); + verify(supplier, atLeast(1)).fetchResource(eq(testCustomResource)); } @Test public void propagateEventOnDeletedResource() throws InterruptedException { source.onResourceCreated(testCustomResource); - when(supplier.getResource(any())) + when(supplier.fetchResource(any())) .thenReturn(Optional.of(SampleExternalResource.testResource1())) .thenReturn(Optional.empty()); Thread.sleep(3 * PERIOD); - verify(supplier, atLeast(2)).getResource(eq(testCustomResource)); + verify(supplier, atLeast(2)).fetchResource(eq(testCustomResource)); verify(eventHandler, times(2)).handleEvent(any()); } @Test public void getsValueFromCacheOrSupplier() throws InterruptedException { source.onResourceCreated(testCustomResource); - when(supplier.getResource(any())) + when(supplier.fetchResource(any())) .thenReturn(Optional.empty()) .thenReturn(Optional.of(SampleExternalResource.testResource1())); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java index d01bd3c747..59d9d4fb91 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResource.java @@ -18,4 +18,5 @@ protected TestCustomResourceSpec initSpec() { protected TestCustomResourceStatus initStatus() { return new TestCustomResourceStatus(); } + } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index ae41eacb68..015f7a18bd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -41,37 +41,38 @@ public UpdateControl reconcile( .get(); if (configMap == null) { var configMapToCreate = createConfigMap(resource); + final var resourceID = ResourceID.fromResource(configMapToCreate); try { - informerEventSource.prepareForCreateOrUpdateEventFiltering( - ResourceID.fromResource(configMapToCreate)); + informerEventSource.prepareForCreateOrUpdateEventFiltering(resourceID, configMapToCreate); configMap = client .configMaps() .inNamespace(resource.getMetadata().getNamespace()) .create(configMapToCreate); - informerEventSource.handleRecentResourceCreate(configMap); + informerEventSource.handleRecentResourceCreate(resourceID, configMap); } catch (RuntimeException e) { informerEventSource - .cleanupOnCreateOrUpdateEventFiltering(ResourceID.fromResource(configMapToCreate)); + .cleanupOnCreateOrUpdateEventFiltering(resourceID, configMapToCreate); throw e; } } else { + ResourceID resourceID = ResourceID.fromResource(configMap); if (!Objects.equals( configMap.getData().get(CONFIG_MAP_TEST_DATA_KEY), resource.getSpec().getValue())) { configMap.getData().put(CONFIG_MAP_TEST_DATA_KEY, resource.getSpec().getValue()); try { informerEventSource - .prepareForCreateOrUpdateEventFiltering(ResourceID.fromResource(configMap)); + .prepareForCreateOrUpdateEventFiltering(resourceID, configMap); var newConfigMap = client .configMaps() .inNamespace(resource.getMetadata().getNamespace()) .replace(configMap); - informerEventSource.handleRecentResourceUpdate( - newConfigMap, configMap.getMetadata().getResourceVersion()); + informerEventSource.handleRecentResourceUpdate(resourceID, + newConfigMap, configMap); } catch (RuntimeException e) { informerEventSource - .cleanupOnCreateOrUpdateEventFiltering(ResourceID.fromResource(configMap)); + .cleanupOnCreateOrUpdateEventFiltering(resourceID, configMap); throw e; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index b6efb0f579..12b7f9506e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; @@ -33,7 +34,7 @@ public StandaloneDependentTestReconciler() { @Override public List prepareEventSources( EventSourceContext context) { - return List.of(deploymentDependent.eventSource(context)); + return List.of(deploymentDependent.initEventSource(context)); } @Override @@ -66,6 +67,10 @@ public KubernetesClient getKubernetesClient() { @Override public Optional updateErrorStatus( StandaloneDependentTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + // this can happen when a namespace is terminated in test + if (e instanceof KubernetesClientException) { + return Optional.empty(); + } errorOccurred = true; return Optional.empty(); } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index a6923823aa..140047a3b4 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -41,7 +41,7 @@ public UpdateControl reconcile(MySQLSchema schema, Context context) Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); SchemaDependentResource schemaDependentResource = context.managedDependentResourceContext() .getDependentResource(SchemaDependentResource.class); - return schemaDependentResource.getResource(schema).map(s -> { + return schemaDependentResource.fetchResource(schema).map(s -> { updateStatusPojo(schema, secret.getMetadata().getName(), secret.getData().get(MYSQL_SECRET_USERNAME)); log.info("Schema {} created - updating CR status", schema.getMetadata().getName()); 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 0a97684e0d..f1fbc4b642 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -11,14 +11,11 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; +import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; import io.javaoperatorsdk.operator.sample.*; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; @@ -28,7 +25,7 @@ import static java.lang.String.format; public class SchemaDependentResource - extends AbstractDependentResource + extends PerResourcePollingDependentResource implements EventSourceProvider, DependentResourceConfigurator, Creator, @@ -37,22 +34,11 @@ public class SchemaDependentResource private static final Logger log = LoggerFactory.getLogger(SchemaDependentResource.class); private MySQLDbConfig dbConfig; - private int pollPeriod = 500; @Override public void configureWith(ResourcePollerConfig config) { this.dbConfig = config.getMySQLDbConfig(); - this.pollPeriod = config.getPollPeriod(); - } - - @Override - public EventSource eventSource(EventSourceContext context) { - if (dbConfig == null) { - dbConfig = MySQLDbConfig.loadFromEnvironmentVars(); - } - return new PerResourcePollingEventSource<>( - new SchemaPollingResourceSupplier(dbConfig), context.getPrimaryCache(), pollPeriod, - Schema.class); + setPollingPeriod(config.getPollPeriod()); } @Override @@ -61,12 +47,12 @@ public Schema desired(MySQLSchema primary, Context context) { } @Override - public void create(Schema target, MySQLSchema mySQLSchema, Context context) { + public Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); var username = decode(secret.getData().get(MYSQL_SECRET_USERNAME)); var password = decode(secret.getData().get(MYSQL_SECRET_PASSWORD)); - final var schema = SchemaService.createSchemaAndRelatedUser( + return SchemaService.createSchemaAndRelatedUser( connection, target.getName(), target.getCharacterSet(), username, password); @@ -94,9 +80,8 @@ public void delete(MySQLSchema primary, Context context) { } } - // todo this should read the resource from event source? @Override - public Optional getResource(MySQLSchema primaryResource) { + public Optional fetchResource(MySQLSchema primaryResource) { try (Connection connection = getConnection()) { var schema = SchemaService.getSchema(connection, primaryResource.getMetadata().getName()).orElse(null); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceFetcher.java similarity index 70% rename from sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java rename to sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceFetcher.java index acf620e819..2830a4b08b 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceSupplier.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaPollingResourceFetcher.java @@ -8,17 +8,17 @@ import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; -public class SchemaPollingResourceSupplier - implements PerResourcePollingEventSource.ResourceSupplier { +public class SchemaPollingResourceFetcher + implements PerResourcePollingEventSource.ResourceFetcher { private final SchemaService schemaService; - public SchemaPollingResourceSupplier(MySQLDbConfig mySQLDbConfig) { + public SchemaPollingResourceFetcher(MySQLDbConfig mySQLDbConfig) { this.schemaService = new SchemaService(mySQLDbConfig); } @Override - public Optional getResource(MySQLSchema resource) { + public Optional fetchResource(MySQLSchema resource) { return schemaService.getSchema(resource.getMetadata().getName()); } } 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 8f9302a81a..ed0ff40489 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 @@ -83,7 +83,7 @@ boolean isLocal() { public MySQLSchemaOperatorE2E() throws FileNotFoundException {} @Test - public void test() throws IOException { + void test() throws IOException { // Opening a port-forward if running locally LocalPortForward portForward = null; if (isLocal()) { diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index 60ef0bc187..dabcbdfa4b 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -23,7 +23,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; -public class TomcatOperatorE2E { +class TomcatOperatorE2E { final static Logger log = LoggerFactory.getLogger(TomcatOperatorE2E.class); @@ -80,7 +80,7 @@ Webapp getWebapp() { } @Test - public void test() { + void test() { var tomcat = getTomcat(); var webapp1 = getWebapp(); var tomcatClient = client.resources(Tomcat.class); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index cb5144a8d3..b4f6db1395 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -45,9 +45,9 @@ public WebPageReconcilerDependentResources(KubernetesClient kubernetesClient) { @Override public List prepareEventSources(EventSourceContext context) { return List.of( - configMapDR.eventSource(context), - deploymentDR.eventSource(context), - serviceDR.eventSource(context)); + configMapDR.initEventSource(context), + deploymentDR.initEventSource(context), + serviceDR.initEventSource(context)); } @Override @@ -180,8 +180,8 @@ protected ConfigMap desired(WebPage webPage, Context context) { } @Override - public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { - super.update(actual, target, primary, context); + public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + var res = super.update(actual, target, primary, context); var ns = actual.getMetadata().getNamespace(); log.info("Restarting pods because HTML has changed in {}", ns); kubernetesClient @@ -189,6 +189,7 @@ public void update(ConfigMap actual, ConfigMap target, WebPage primary, Context .inNamespace(ns) .withLabel("app", deploymentName(primary)) .delete(); + return res; } @Override From 2db40f0b71fbcb1b2c9a5555534cf1d7a7342386 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 09:10:47 +0100 Subject: [PATCH 0334/1608] chore(deps): bump awaitility from 4.1.1 to 4.2.0 (#999) --- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 5412cfac2a..aebabe2416 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ 0.19 1.13.0 3.22.0 - 4.1.1 + 4.2.0 2.6.4 1.8.3 diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 7d25dc2f5f..5f96f8af9a 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -64,7 +64,7 @@ org.awaitility awaitility - 4.1.0 + 4.2.0 test diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 4381948214..7b41dfb4b2 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -54,7 +54,7 @@ org.awaitility awaitility - 4.1.1 + 4.2.0 test From 496adeb4a626ef151e90ef3f1891e1883298608c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 09:11:03 +0100 Subject: [PATCH 0335/1608] chore(deps): bump jackson-dataformat-yaml from 2.13.1 to 2.13.2 (#998) Bumps [jackson-dataformat-yaml](https://github.com/FasterXML/jackson-dataformats-text) from 2.13.1 to 2.13.2. - [Release notes](https://github.com/FasterXML/jackson-dataformats-text/releases) - [Commits](https://github.com/FasterXML/jackson-dataformats-text/compare/jackson-dataformats-text-2.13.1...jackson-dataformats-text-2.13.2) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sample-operators/mysql-schema/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 5f96f8af9a..6cf30996a6 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -70,7 +70,7 @@ com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 2.13.1 + 2.13.2 io.javaoperatorsdk From 297b0848391e24be5eca9fc6a576d46f4293eede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 7 Mar 2022 11:14:49 +0100 Subject: [PATCH 0336/1608] fix: javadoc for fails snapshot release (#1000) --- .../external/AbstractSimpleDependentResource.java | 7 ++++++- .../event/ExternalResourceCachingEventSource.java | 3 +-- .../processing/event/source/CachingEventSource.java | 6 ------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index 18de3390e0..564646b217 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -34,7 +34,12 @@ public Optional getResource(HasMetadata primaryResource) { return cache.get(ResourceID.fromResource(primaryResource)); } - /** Actually read the resource from the target API */ + /** + * Actually read the resource from the target API + * + * @param primaryResource the primary associated resource + * @return fetched resource if present + **/ public abstract Optional fetchResource(HasMetadata primaryResource); @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java index 5ffa9b2166..f69b33dd43 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ExternalResourceCachingEventSource.java @@ -5,8 +5,7 @@ import io.javaoperatorsdk.operator.processing.event.source.CachingEventSource; public class ExternalResourceCachingEventSource - extends CachingEventSource - implements RecentOperationCacheFiller { + extends CachingEventSource implements RecentOperationCacheFiller { public ExternalResourceCachingEventSource(Class resourceClass) { super(resourceClass); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java index cfc065331d..57d94d8922 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/CachingEventSource.java @@ -10,12 +10,6 @@ /** * Base class for event sources with caching capabilities. *

- * {@link #handleDelete(ResourceID)} - if the related resource is present in the cache it is removed - * and event propagated. There is no event propagated if the resource is not in the cache. - *

- * {@link #handleEvent(Object, ResourceID)} - caches the resource if changed or missing. Propagates - * an event if the resource is new or not equals to the one in the cache, and if accepted by the - * filter if one is present. * * @param represents the type of resources (usually external non-kubernetes ones) being handled. */ From 95ac79f75e3d99850b6cfd56d7b64de9a0eeaa74 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 7 Mar 2022 17:27:21 +0100 Subject: [PATCH 0337/1608] fix: state controller name in exception (#1002) Fixes #1001 --- .../operator/processing/event/EventSourceManager.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 6e0b218d55..fda64660b0 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 @@ -129,12 +129,14 @@ public final void registerEventSource(EventSource eventSource) } catch (IllegalStateException | MissingCRDException e) { throw e; // leave untouched } catch (Exception e) { - throw new OperatorException("Couldn't register event source: " + eventSource.name(), e); + throw new OperatorException("Couldn't register event source: " + eventSource.name() + " for " + + controller.getConfiguration().getName() + " controller`", e); } finally { lock.unlock(); } } + @SuppressWarnings("unchecked") public void broadcastOnResourceEvent(ResourceAction action, R resource, R oldResource) { for (var eventSource : eventSources) { if (eventSource instanceof ResourceEventAware) { @@ -211,8 +213,8 @@ public Iterator iterator() { } public Set all() { - return new LinkedHashSet<>(sources.values().stream().flatMap(Collection::stream) - .collect(Collectors.toList())); + return sources.values().stream().flatMap(Collection::stream) + .collect(Collectors.toCollection(LinkedHashSet::new)); } public void clear() { @@ -236,6 +238,7 @@ public void add(EventSource eventSource) { sources.computeIfAbsent(keyFor(eventSource), k -> new ArrayList<>()).add(eventSource); } + @SuppressWarnings("rawtypes") private Class getDependentType(EventSource source) { return source instanceof ResourceEventSource ? ((ResourceEventSource) source).getResourceClass() @@ -265,6 +268,7 @@ private String keyFor(Class dependentType) { return key; } + @SuppressWarnings("unchecked") public ResourceEventSource get(Class dependentType, String name) { final var sourcesForType = sources.get(keyFor(dependentType)); if (sourcesForType == null || sourcesForType.isEmpty()) { From 551ea02dfd9ce4d6116c5cc16cbdd107a8e4bea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 8 Mar 2022 21:19:13 +0100 Subject: [PATCH 0338/1608] fix: added logging (#1005) --- .../operator/processing/event/EventProcessor.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 9ea6df7332..7f4cae69a4 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 @@ -168,8 +168,10 @@ private void submitReconciliationExecution(ResourceID resourceID) { private void handleEventMarking(Event event) { if (event instanceof ResourceEvent && ((ResourceEvent) event).getAction() == ResourceAction.DELETED) { + log.debug("Marking delete event received for: {}", event.getRelatedCustomResourceID()); eventMarker.markDeleteEventReceived(event); } else if (!eventMarker.deleteEventPresent(event.getRelatedCustomResourceID())) { + log.debug("Marking event received for: {}", event.getRelatedCustomResourceID()); eventMarker.markEventReceived(event); } } @@ -227,7 +229,13 @@ private void reScheduleExecutionIfInstructed( PostExecutionControl postExecutionControl, R customResource) { postExecutionControl .getReScheduleDelay() - .ifPresent(delay -> retryEventSource().scheduleOnce(customResource, delay)); + .ifPresent(delay -> { + if (log.isDebugEnabled()) { + log.debug("ReScheduling event for resource: {} with delay: {}", + ResourceID.fromResource(customResource), delay); + } + retryEventSource().scheduleOnce(customResource, delay); + }); } TimerEventSource retryEventSource() { @@ -284,6 +292,7 @@ private RetryExecution getOrInitRetryExecution(ExecutionScope executionScope) } private void cleanupForDeletedEvent(ResourceID customResourceUid) { + log.debug("Cleaning up for delete event for: {}", customResourceUid); eventMarker.cleanup(customResourceUid); metrics.cleanupDoneFor(customResourceUid); } From bf075d75616457e43c1d764ce92b46b1f8255fb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 11:28:53 +0100 Subject: [PATCH 0339/1608] chore(deps-dev): bump mockito-core from 4.3.1 to 4.4.0 (#1007) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.3.1 to 4.4.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.3.1...v4.4.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aebabe2416..dee39368d9 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 5.12.0 1.7.36 2.17.2 - 4.3.1 + 4.4.0 3.12.0 1.0.1 0.19 From 1c6b283183306219ce17dd4ddf8010ddbb9b017d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 9 Mar 2022 21:09:41 +0100 Subject: [PATCH 0340/1608] fix: making sure there is not race condition with temporal cache (#1009) --- .../ControllerResourceEventSource.java | 6 +- .../event/source/informer/EventRecorder.java | 9 +- .../source/informer/InformerEventSource.java | 9 +- .../informer/TemporaryResourceCache.java | 83 +++++++------------ 4 files changed, 40 insertions(+), 67 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 801eae09ee..464cc3e500 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -48,10 +48,8 @@ public ControllerResourceEventSource(Controller controller) { public void start() { try { super.start(); - } catch (Exception e) { - if (e instanceof KubernetesClientException) { - handleKubernetesClientException(e); - } + } catch (KubernetesClientException e) { + handleKubernetesClientException(e); throw e; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java index 284b749f07..5d23d870aa 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/EventRecorder.java @@ -1,18 +1,18 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; public class EventRecorder { - private final Map> resourceEvents = new ConcurrentHashMap<>(); + private final Map> resourceEvents = new HashMap<>(); - void startEventRecording(ResourceID resourceID) { + public void startEventRecording(ResourceID resourceID) { resourceEvents.putIfAbsent(resourceID, new ArrayList<>(5)); } @@ -28,7 +28,8 @@ public void recordEvent(R resource) { resourceEvents.get(ResourceID.fromResource(resource)).add(resource); } - public boolean containsEventWithResourceVersion(ResourceID resourceID, String resourceVersion) { + public boolean containsEventWithResourceVersion(ResourceID resourceID, + String resourceVersion) { List events = resourceEvents.get(resourceID); if (events == null) { return false; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 4a23bd9363..e1ef859549 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -69,6 +69,7 @@ public class InformerEventSource private static final Logger log = LoggerFactory.getLogger(InformerEventSource.class); private final InformerConfiguration configuration; + // always called from a synchronized method private final EventRecorder eventRecorder = new EventRecorder<>(); public InformerEventSource( @@ -161,7 +162,7 @@ public InformerConfiguration getConfiguration() { } @Override - public void handleRecentResourceUpdate(ResourceID resourceID, R resource, + public synchronized void handleRecentResourceUpdate(ResourceID resourceID, R resource, R previousResourceVersion) { handleRecentCreateOrUpdate(resource, () -> super.handleRecentResourceUpdate(resourceID, resource, @@ -169,12 +170,12 @@ public void handleRecentResourceUpdate(ResourceID resourceID, R resource, } @Override - public void handleRecentResourceCreate(ResourceID resourceID, R resource) { + public synchronized void handleRecentResourceCreate(ResourceID resourceID, R resource) { handleRecentCreateOrUpdate(resource, () -> super.handleRecentResourceCreate(resourceID, resource)); } - private synchronized void handleRecentCreateOrUpdate(R resource, Runnable runnable) { + private void handleRecentCreateOrUpdate(R resource, Runnable runnable) { if (eventRecorder.isRecordingFor(ResourceID.fromResource(resource))) { handleRecentResourceOperationAndStopEventRecording(resource); } else { @@ -199,7 +200,7 @@ private synchronized void handleRecentCreateOrUpdate(R resource, Runnable runnab * * @param resource just created or updated resource */ - private synchronized void handleRecentResourceOperationAndStopEventRecording(R resource) { + private void handleRecentResourceOperationAndStopEventRecording(R resource) { ResourceID resourceID = ResourceID.fromResource(resource); try { if (!eventRecorder.containsEventWithResourceVersion( 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 fb14ea6847..51a5a73bd4 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,7 +3,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,75 +35,49 @@ public class TemporaryResourceCache { private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); private final Map cache = new ConcurrentHashMap<>(); - private final ReentrantLock lock = new ReentrantLock(); private final ManagedInformerEventSource managedInformerEventSource; public TemporaryResourceCache(ManagedInformerEventSource managedInformerEventSource) { this.managedInformerEventSource = managedInformerEventSource; } - public void removeResourceFromCache(T resource) { - lock.lock(); - try { - cache.remove(ResourceID.fromResource(resource)); - } finally { - lock.unlock(); - } + public synchronized void removeResourceFromCache(T resource) { + cache.remove(ResourceID.fromResource(resource)); } - public void unconditionallyCacheResource(T newResource) { - lock.lock(); - try { - cache.put(ResourceID.fromResource(newResource), newResource); - } finally { - lock.unlock(); - } + public synchronized void unconditionallyCacheResource(T newResource) { + cache.put(ResourceID.fromResource(newResource), newResource); } - public void putAddedResource(T newResource) { - lock.lock(); - try { - ResourceID resourceID = ResourceID.fromResource(newResource); - if (managedInformerEventSource.get(resourceID).isEmpty()) { - log.debug("Putting resource to cache with ID: {}", resourceID); - cache.put(ResourceID.fromResource(newResource), newResource); - } else { - log.debug("Won't put resource into cache found already informer cache: {}", resourceID); - } - } finally { - lock.unlock(); + 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.fromResource(newResource), newResource); + } else { + log.debug("Won't put resource into cache found already informer cache: {}", resourceID); } } - public void putUpdatedResource(T newResource, String previousResourceVersion) { - lock.lock(); - try { - var resourceId = ResourceID.fromResource(newResource); - var informerCacheResource = managedInformerEventSource.get(resourceId); - if (informerCacheResource.isEmpty()) { - log.debug("No cached value present for resource: {}", newResource); - return; - } - // if this is not true that means the cache was already updated - if (informerCacheResource.get().getMetadata().getResourceVersion() - .equals(previousResourceVersion)) { - log.debug("Putting resource to temporal cache with id: {}", resourceId); - cache.put(resourceId, newResource); - } else { - // if something is in cache it's surely obsolete now - cache.remove(resourceId); - } - } finally { - lock.unlock(); + public synchronized void putUpdatedResource(T newResource, String previousResourceVersion) { + var resourceId = ResourceID.fromResource(newResource); + var informerCacheResource = managedInformerEventSource.get(resourceId); + if (informerCacheResource.isEmpty()) { + log.debug("No cached value present for resource: {}", newResource); + return; + } + // if this is not true that means the cache was already updated + if (informerCacheResource.get().getMetadata().getResourceVersion() + .equals(previousResourceVersion)) { + log.debug("Putting resource to temporal cache with id: {}", resourceId); + cache.put(resourceId, newResource); + } else { + // if something is in cache it's surely obsolete now + cache.remove(resourceId); } } - public Optional getResourceFromCache(ResourceID resourceID) { - try { - lock.lock(); - return Optional.ofNullable(cache.get(resourceID)); - } finally { - lock.unlock(); - } + public synchronized Optional getResourceFromCache(ResourceID resourceID) { + return Optional.ofNullable(cache.get(resourceID)); } } From acd9ec4b2675e387ff21f0efc5db485f4161dc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 9 Mar 2022 21:10:25 +0100 Subject: [PATCH 0341/1608] Race condition fix (#1011) --- .../operator/processing/Controller.java | 2 +- .../processing/event/EventProcessor.java | 50 ++------------ .../ControllerResourceEventSource.java | 67 +++++++++++-------- .../OnceWhitelistEventFilterEventFilter.java | 35 ---------- .../controller/TemporaryResourceCache.java | 50 ++++++++++++++ .../processing/event/EventProcessorTest.java | 63 ++--------------- ...ceWhitelistEventFilterEventFilterTest.java | 41 ------------ .../ControllerResourceEventSourceTest.java | 22 ------ 8 files changed, 101 insertions(+), 229 deletions(-) delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/TemporaryResourceCache.java delete mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java 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 f9a600f1a7..704945925f 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 @@ -188,7 +188,7 @@ public void start() throws OperatorException { if (reconciler instanceof EventSourceInitializer) { ((EventSourceInitializer) reconciler) .prepareEventSources(new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), configurationService(), kubernetesClient)) .forEach(eventSourceManager::registerEventSource); } 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 29f9adab55..a88d80783d 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 @@ -29,7 +29,6 @@ import io.javaoperatorsdk.operator.processing.retry.RetryExecution; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; -import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion; class EventProcessor implements EventHandler, LifecycleAware { @@ -50,7 +49,7 @@ class EventProcessor implements EventHandler, LifecycleAw EventProcessor(EventSourceManager eventSourceManager) { this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), ExecutorServiceManager.instance().executorService(), eventSourceManager.getController().getConfiguration().getName(), new ReconciliationDispatcher<>(eventSourceManager.getController()), @@ -73,7 +72,7 @@ class EventProcessor implements EventHandler, LifecycleAw Retry retry, Metrics metrics) { this( - eventSourceManager.getControllerResourceEventSource().getResourceCache(), + eventSourceManager.getControllerResourceEventSource(), null, relatedControllerName, reconciliationDispatcher, @@ -208,12 +207,12 @@ void eventProcessingFinished( if (eventMarker.deleteEventPresent(resourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { + postExecutionControl.getUpdatedCustomResource().ifPresent(r -> { + eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate( + r, executionScope.getResource()); + }); if (eventMarker.eventPresent(resourceID)) { - if (isCacheReadyForInstantReconciliation(executionScope, postExecutionControl)) { - submitReconciliationExecution(resourceID); - } else { - postponeReconciliationAndHandleCacheSyncEvent(resourceID); - } + submitReconciliationExecution(resourceID); } else { reScheduleExecutionIfInstructed(postExecutionControl, executionScope.getResource()); } @@ -223,41 +222,6 @@ void eventProcessingFinished( } } - private void postponeReconciliationAndHandleCacheSyncEvent(ResourceID resourceID) { - eventSourceManager.getControllerResourceEventSource().whitelistNextEvent(resourceID); - } - - private boolean isCacheReadyForInstantReconciliation( - ExecutionScope executionScope, PostExecutionControl postExecutionControl) { - if (!postExecutionControl.customResourceUpdatedDuringExecution()) { - return true; - } - String originalResourceVersion = getVersion(executionScope.getResource()); - String customResourceVersionAfterExecution = - getVersion( - postExecutionControl - .getUpdatedCustomResource() - .orElseThrow( - () -> new IllegalStateException( - "Updated custom resource must be present at this point of time"))); - String cachedCustomResourceVersion = - getVersion( - cache - .get(executionScope.getCustomResourceID()) - .orElseThrow( - () -> new IllegalStateException( - "Cached custom resource must be present at this point"))); - - if (cachedCustomResourceVersion.equals(customResourceVersionAfterExecution)) { - return true; - } - // If the cached resource version equals neither the version before nor after execution - // probably an update happened on the custom resource independent of the framework during - // reconciliation. We cannot tell at this point if it happened before our update or before. - // (Well we could if we would parse resource version, but that should not be done by definition) - return !cachedCustomResourceVersion.equals(originalResourceVersion); - } - private void reScheduleExecutionIfInstructed( PostExecutionControl postExecutionControl, R customResource) { postExecutionControl diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 9feabf40bf..e2b4e89ebd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -5,6 +5,8 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +24,7 @@ import io.javaoperatorsdk.operator.processing.MDCUtils; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName; import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID; @@ -29,7 +32,7 @@ public class ControllerResourceEventSource extends AbstractResourceEventSource - implements ResourceEventHandler { + implements ResourceEventHandler, ResourceCache { public static final String ANY_NAMESPACE_MAP_KEY = "anyNamespace"; @@ -40,8 +43,8 @@ public class ControllerResourceEventSource new ConcurrentHashMap<>(); private final ResourceEventFilter filter; - private final OnceWhitelistEventFilterEventFilter onceWhitelistEventFilterEventFilter; private final ControllerResourceCache cache; + private final TemporaryResourceCache temporaryResourceCache; public ControllerResourceEventSource(Controller controller) { super(controller.getConfiguration().getResourceClass()); @@ -50,20 +53,12 @@ public ControllerResourceEventSource(Controller controller) { var cloner = configurationService != null ? configurationService.getResourceCloner() : ConfigurationService.DEFAULT_CLONER; this.cache = new ControllerResourceCache<>(sharedIndexInformers, cloner); - + temporaryResourceCache = new TemporaryResourceCache<>(cache); var filters = new ResourceEventFilter[] { ResourceEventFilters.finalizerNeededAndApplied(), ResourceEventFilters.markedForDeletion(), - ResourceEventFilters.generationAware(), - null + ResourceEventFilters.generationAware() }; - - if (controller.getConfiguration().isGenerationAware()) { - onceWhitelistEventFilterEventFilter = new OnceWhitelistEventFilterEventFilter<>(); - filters[filters.length - 1] = onceWhitelistEventFilterEventFilter; - } else { - onceWhitelistEventFilterEventFilter = null; - } if (controller.getConfiguration().getEventFilter() != null) { filter = controller.getConfiguration().getEventFilter().and(ResourceEventFilters.or(filters)); } else { @@ -126,6 +121,7 @@ public void eventReceived(ResourceAction action, T customResource, T oldResource try { log.debug( "Event received for resource: {}", getName(customResource)); + temporaryResourceCache.removeResourceFromCache(customResource); MDCUtils.addResourceInfo(customResource); controller.getEventSourceManager().broadcastOnResourceEvent(action, customResource, oldResource); @@ -158,12 +154,31 @@ public void onDelete(T resource, boolean b) { eventReceived(ResourceAction.DELETED, resource, null); } + + @Override public Optional get(ResourceID resourceID) { - return cache.get(resourceID); + Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); + if (resource.isPresent()) { + log.debug("Resource found in temporal cache for Resource ID: {}", resourceID); + return resource; + } else { + return cache.get(resourceID); + } + } + + @Override + public Stream keys() { + return cache.keys(); + } + + @Override + public Stream list(Predicate predicate) { + return cache.list(predicate); } - public ControllerResourceCache getResourceCache() { - return cache; + @Override + public Stream list(String namespace, Predicate predicate) { + return cache.list(namespace, predicate); } /** @@ -178,19 +193,6 @@ public SharedIndexInformer getInformer(String namespace) { return getInformers().get(Objects.requireNonNullElse(namespace, ANY_NAMESPACE_MAP_KEY)); } - /** - * This will ensure that the next event received after this method is called will not be filtered - * out. - * - * @param resourceID - to which the event is related - */ - public void whitelistNextEvent(ResourceID resourceID) { - if (onceWhitelistEventFilterEventFilter != null) { - onceWhitelistEventFilterEventFilter.whitelistNextEvent(resourceID); - } - } - - private void handleKubernetesClientException(Exception e) { KubernetesClientException ke = (KubernetesClientException) e; if (404 == ke.getCode()) { @@ -204,6 +206,13 @@ private void handleKubernetesClientException(Exception e) { @Override public Optional getAssociated(T primary) { - return cache.get(ResourceID.fromResource(primary)); + return get(ResourceID.fromResource(primary)); } + + public void handleRecentResourceUpdate(T resource, + T previousResourceVersion) { + temporaryResourceCache.putUpdatedResource(resource, + previousResourceVersion.getMetadata().getResourceVersion()); + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java deleted file mode 100644 index 8262ff1c21..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/OnceWhitelistEventFilterEventFilter.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source.controller; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.processing.event.ResourceID; - -public class OnceWhitelistEventFilterEventFilter - implements ResourceEventFilter { - - private static final Logger log = - LoggerFactory.getLogger(OnceWhitelistEventFilterEventFilter.class); - - private final ConcurrentMap whiteList = new ConcurrentHashMap<>(); - - @Override - public boolean acceptChange(ControllerConfiguration configuration, T oldResource, - T newResource) { - ResourceID resourceID = ResourceID.fromResource(newResource); - boolean res = whiteList.remove(resourceID, resourceID); - if (res) { - log.debug("Accepting whitelisted event for CR id: {}", resourceID); - } - return res; - } - - public void whitelistNextEvent(ResourceID resourceID) { - whiteList.putIfAbsent(resourceID, resourceID); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/TemporaryResourceCache.java new file mode 100644 index 0000000000..fb402b0b86 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/TemporaryResourceCache.java @@ -0,0 +1,50 @@ +package io.javaoperatorsdk.operator.processing.event.source.controller; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class TemporaryResourceCache { + + private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); + + private final Map cache = new HashMap<>(); + private final ControllerResourceCache managedInformerEventSource; + + public TemporaryResourceCache(ControllerResourceCache managedInformerEventSource) { + this.managedInformerEventSource = managedInformerEventSource; + } + + public synchronized void removeResourceFromCache(T resource) { + cache.remove(ResourceID.fromResource(resource)); + } + + public synchronized void putUpdatedResource(T newResource, String previousResourceVersion) { + var resourceId = ResourceID.fromResource(newResource); + var informerCacheResource = managedInformerEventSource.get(resourceId); + if (informerCacheResource.isEmpty()) { + log.debug("No cached value present for resource: {}", newResource); + return; + } + // if this is not true that means the cache was already updated + if (informerCacheResource.get().getMetadata().getResourceVersion() + .equals(previousResourceVersion)) { + log.debug("Putting resource to temporal cache with id: {}", resourceId); + cache.put(resourceId, newResource); + } else { + // if something is in cache it's surely obsolete now + cache.remove(resourceId); + } + } + + public synchronized Optional getResourceFromCache(ResourceID resourceID) { + return Optional.ofNullable(cache.get(resourceID)); + } +} + 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 5a3f73742e..7422e6f572 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 @@ -13,7 +13,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceCache; import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerResourceEventSource; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent; @@ -44,8 +43,6 @@ class EventProcessorTest { private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); - private ControllerResourceCache resourceCacheMock = - mock(ControllerResourceCache.class); private TimerEventSource retryTimerEventSourceMock = mock(TimerEventSource.class); private ControllerResourceEventSource controllerResourceEventSourceMock = mock(ControllerResourceEventSource.class); @@ -58,7 +55,6 @@ public void setup() { when(eventSourceManagerMock.getControllerResourceEventSource()) .thenReturn(controllerResourceEventSourceMock); - when(controllerResourceEventSourceMock.getResourceCache()).thenReturn(resourceCacheMock); eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, @@ -83,7 +79,8 @@ public void dispatchesEventsIfNoExecutionInProgress() { @Test public void skipProcessingIfLatestCustomResourceNotInCache() { Event event = prepareCREvent(); - when(resourceCacheMock.get(event.getRelatedCustomResourceID())).thenReturn(Optional.empty()); + when(controllerResourceEventSourceMock.get(event.getRelatedCustomResourceID())) + .thenReturn(Optional.empty()); eventProcessor.handleEvent(event); @@ -214,57 +211,6 @@ public void doNotFireEventsIfClosing() { verify(reconciliationDispatcherMock, timeout(50).times(0)).handleExecution(any()); } - @Test - public void whitelistNextEventIfTheCacheIsNotPropagatedAfterAnUpdate() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var updatedCr = testCustomResource(crID); - updatedCr.getMetadata().setResourceVersion("2"); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(updatedCr)); - - verify(mockCREventSource, times(1)).whitelistNextEvent(eq(crID)); - } - - @Test - public void dontWhitelistsEventWhenOtherChangeDuringExecution() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var updatedCr = testCustomResource(crID); - updatedCr.getMetadata().setResourceVersion("2"); - var otherChangeCR = testCustomResource(crID); - otherChangeCR.getMetadata().setResourceVersion("3"); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(otherChangeCR)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(updatedCr)); - - verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); - } - - @Test - public void dontWhitelistsEventIfUpdatedEventInCache() { - var crID = new ResourceID("test-cr", TEST_NAMESPACE); - var cr = testCustomResource(crID); - var mockCREventSource = mock(ControllerResourceEventSource.class); - eventProcessor.getEventMarker().markEventReceived(crID); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(cr)); - when(eventSourceManagerMock.getControllerResourceEventSource()).thenReturn(mockCREventSource); - - eventProcessor.eventProcessingFinished(new ExecutionScope(cr, null), - PostExecutionControl.customResourceUpdated(cr)); - - verify(mockCREventSource, times(0)).whitelistNextEvent(eq(crID)); - } - @Test public void cancelScheduleOnceEventsOnSuccessfulExecution() { var crID = new ResourceID("test-cr", TEST_NAMESPACE); @@ -282,7 +228,8 @@ public void startProcessedMarkedEventReceivedBefore() { eventProcessor = spy(new EventProcessor(reconciliationDispatcherMock, eventSourceManagerMock, "Test", null, metricsMock)); - when(resourceCacheMock.get(eq(crID))).thenReturn(Optional.of(testCustomResource())); + when(controllerResourceEventSourceMock.get(eq(crID))) + .thenReturn(Optional.of(testCustomResource())); eventProcessor.handleEvent(new Event(crID)); verify(reconciliationDispatcherMock, timeout(100).times(0)).handleExecution(any()); @@ -311,7 +258,7 @@ private ResourceEvent prepareCREvent() { private ResourceEvent prepareCREvent(ResourceID uid) { TestCustomResource customResource = testCustomResource(uid); - when(resourceCacheMock.get(eq(uid))).thenReturn(Optional.of(customResource)); + when(controllerResourceEventSourceMock.get(eq(uid))).thenReturn(Optional.of(customResource)); return new ResourceEvent(ResourceAction.UPDATED, ResourceID.fromResource(customResource)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java deleted file mode 100644 index f82bea55c7..0000000000 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/OnceWhitelistEventFilterEventFilterTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.javaoperatorsdk.operator.processing.event.source; - -import org.junit.jupiter.api.Test; - -import io.javaoperatorsdk.operator.TestUtils; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.controller.OnceWhitelistEventFilterEventFilter; - -import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; -import static org.assertj.core.api.Assertions.assertThat; - -class OnceWhitelistEventFilterEventFilterTest { - - private OnceWhitelistEventFilterEventFilter filter = new OnceWhitelistEventFilterEventFilter<>(); - - @Test - public void notAcceptCustomResourceNotWhitelisted() { - assertThat(filter.acceptChange(null, - testCustomResource(), testCustomResource())).isFalse(); - } - - @Test - public void allowCustomResourceWhitelisted() { - var cr = TestUtils.testCustomResource(); - - filter.whitelistNextEvent(ResourceID.fromResource(cr)); - - assertThat(filter.acceptChange(null, cr, cr)).isTrue(); - } - - @Test - public void allowCustomResourceWhitelistedOnlyOnce() { - var cr = TestUtils.testCustomResource(); - - filter.whitelistNextEvent(ResourceID.fromResource(cr)); - - assertThat(filter.acceptChange(null, cr, cr)).isTrue(); - assertThat(filter.acceptChange(null, cr, cr)).isFalse(); - } - -} 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 5859115ee2..7f20918f13 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 @@ -14,7 +14,6 @@ import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; -import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.AbstractEventSourceTestBase; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -103,27 +102,6 @@ public void eventWithNoGenerationProcessedIfNoFinalizer() { verify(eventHandler, times(1)).handleEvent(any()); } - @Test - public void handlesNextEventIfWhitelisted() { - TestCustomResource customResource = TestUtils.testCustomResource(); - customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - source.whitelistNextEvent(ResourceID.fromResource(customResource)); - - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); - - verify(eventHandler, times(1)).handleEvent(any()); - } - - @Test - public void notHandlesNextEventIfNotWhitelisted() { - TestCustomResource customResource = TestUtils.testCustomResource(); - customResource.getMetadata().setFinalizers(List.of(FINALIZER)); - - source.eventReceived(ResourceAction.UPDATED, customResource, customResource); - - verify(eventHandler, times(0)).handleEvent(any()); - } - @Test public void callsBroadcastsOnResourceEvents() { TestCustomResource customResource1 = TestUtils.testCustomResource(); From e5ee911605b17b3ef44b497c4b370a1696adceaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 10 Mar 2022 09:15:14 +0100 Subject: [PATCH 0342/1608] fix: issue with reading from cache (#1008) --- .../event/source/controller/ControllerResourceEventSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index 464cc3e500..b43666660c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -102,6 +102,6 @@ private void handleKubernetesClientException(Exception e) { @Override public Optional getAssociated(T primary) { - return manager().get(ResourceID.fromResource(primary)); + return get(ResourceID.fromResource(primary)); } } From c612c2706ec09f9ad8ad6b7f188c716f906ac150 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 10 Mar 2022 10:11:56 +0100 Subject: [PATCH 0343/1608] fix: debugThreadPool & shouldCheckCRDAndValidateLocalModel should work (#1016) Fixes #1014 --- .../operator/api/config/Utils.java | 21 +++++- .../operator/api/config/UtilsTest.java | 64 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java 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 b36c0468cd..06bbcaa7a8 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 @@ -61,10 +61,27 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return Boolean.getBoolean(System.getProperty(CHECK_CRD_ENV_KEY, "true")); + return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, true); } public static boolean debugThreadPool() { - return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); + return getBooleanFromSystemPropsOrDefault(DEBUG_THREAD_POOL_ENV_KEY, false); + } + + static boolean getBooleanFromSystemPropsOrDefault(String propertyName, boolean defaultValue) { + var property = System.getProperty(propertyName); + if (property == null) { + return defaultValue; + } else { + property = property.trim().toLowerCase(); + switch (property) { + case "true": + return true; + case "false": + return false; + default: + return defaultValue; + } + } } } 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 new file mode 100644 index 0000000000..a17eb1ba59 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -0,0 +1,64 @@ +package io.javaoperatorsdk.operator.api.config; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class UtilsTest { + + @Test + void shouldCheckCRDAndValidateLocalModelByDefault() { + assertTrue(Utils.shouldCheckCRDAndValidateLocalModel()); + } + + @Test + void shouldNotDebugThreadPoolByDefault() { + assertFalse(Utils.debugThreadPool()); + } + + @Test + void askingForNonexistentPropertyShouldReturnDefault() { + final var key = "foo"; + assertNull(System.getProperty(key)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } + + @Test + void askingForExistingPropertyShouldReturnItIfBoolean() { + final var key = "foo"; + try { + System.setProperty(key, "true"); + assertNotNull(System.getProperty(key)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, "TruE"); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, " \tTRUE "); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, " \nFalSe \t "); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } finally { + System.clearProperty(key); + } + } + + @Test + void askingForExistingNonBooleanPropertyShouldReturnDefaultValue() { + final var key = "foo"; + try { + System.setProperty(key, "bar"); + assertNotNull(System.getProperty(key)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } finally { + System.clearProperty(key); + } + } +} From d81db18bdce549bdfad5760e1ce0c2c455c4187d Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 10 Mar 2022 10:12:10 +0100 Subject: [PATCH 0344/1608] fix: debugThreadPool & shouldCheckCRDAndValidateLocalModel should work (#1015) Fixes #1014 --- .../operator/api/config/Utils.java | 21 ++++++- .../operator/api/config/UtilsTest.java | 56 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) 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 8d9990ee9c..db882ae09e 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 @@ -64,11 +64,28 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return Boolean.getBoolean(System.getProperty(CHECK_CRD_ENV_KEY, "true")); + return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, true); } public static boolean debugThreadPool() { - return Boolean.getBoolean(System.getProperty(DEBUG_THREAD_POOL_ENV_KEY, "false")); + return getBooleanFromSystemPropsOrDefault(DEBUG_THREAD_POOL_ENV_KEY, false); + } + + static boolean getBooleanFromSystemPropsOrDefault(String propertyName, boolean defaultValue) { + var property = System.getProperty(propertyName); + if (property == null) { + return defaultValue; + } else { + property = property.trim().toLowerCase(); + switch (property) { + case "true": + return true; + case "false": + return false; + default: + return defaultValue; + } + } } public static Class getFirstTypeArgumentFromExtendedClass(Class clazz) { 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 88dd8db469..fbaa28ba23 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 @@ -12,6 +12,62 @@ class UtilsTest { + @Test + void shouldCheckCRDAndValidateLocalModelByDefault() { + assertTrue(Utils.shouldCheckCRDAndValidateLocalModel()); + } + + @Test + void shouldNotDebugThreadPoolByDefault() { + assertFalse(Utils.debugThreadPool()); + } + + @Test + void askingForNonexistentPropertyShouldReturnDefault() { + final var key = "foo"; + assertNull(System.getProperty(key)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } + + @Test + void askingForExistingPropertyShouldReturnItIfBoolean() { + final var key = "foo"; + try { + System.setProperty(key, "true"); + assertNotNull(System.getProperty(key)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, "TruE"); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, " \tTRUE "); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + + System.setProperty(key, " \nFalSe \t "); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } finally { + System.clearProperty(key); + } + } + + @Test + void askingForExistingNonBooleanPropertyShouldReturnDefaultValue() { + final var key = "foo"; + try { + System.setProperty(key, "bar"); + assertNotNull(System.getProperty(key)); + assertFalse(Utils.getBooleanFromSystemPropsOrDefault(key, false)); + assertTrue(Utils.getBooleanFromSystemPropsOrDefault(key, true)); + } finally { + System.clearProperty(key); + } + } + @Test void getsFirstTypeArgumentFromExtendedClass() { Class res = From 808228f1628160ca175176cc6bc50cf89706abe1 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Thu, 10 Mar 2022 10:37:31 +0100 Subject: [PATCH 0345/1608] feat: add non-apt-tied behavior annotation and configuration to core (#1013) * feat: add non-apt-tied behavior annotation and configuration to core Fixes #1012 * fix: extract resource class from associated reconciler --- .../AnnotationControllerConfiguration.java | 186 ++++++++++++++++++ .../api/config/BaseConfigurationService.java | 44 +++++ .../AnnotationControllerConfiguration.java | 168 +--------------- .../runtime/DefaultConfigurationService.java | 47 ++--- 4 files changed, 251 insertions(+), 194 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java new file mode 100644 index 0000000000..5fd1f3dbfd --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -0,0 +1,186 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +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.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; + +@SuppressWarnings("rawtypes") +public class AnnotationControllerConfiguration + implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { + + protected final Reconciler reconciler; + private final ControllerConfiguration annotation; + private ConfigurationService service; + private List specs; + + public AnnotationControllerConfiguration(Reconciler reconciler) { + this.reconciler = reconciler; + this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); + } + + @Override + public String getName() { + return ReconcilerUtils.getNameFor(reconciler); + } + + @Override + public String getFinalizer() { + 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, new String[] {})); + } + + @Override + @SuppressWarnings("unchecked") + public Class getResourceClass() { + return (Class) Utils.getFirstTypeArgumentFromInterface(reconciler.getClass()); + } + + @Override + public String getLabelSelector() { + return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); + } + + @Override + public ConfigurationService getConfigurationService() { + return service; + } + + @Override + public void setConfigurationService(ConfigurationService service) { + this.service = service; + } + + @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 reconciliationMaxInterval() { + if (annotation.reconciliationMaxInterval() != null) { + if (annotation.reconciliationMaxInterval().interval() <= 0) { + return Optional.empty(); + } + return Optional.of( + Duration.of( + annotation.reconciliationMaxInterval().interval(), + annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); + } else { + return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.reconciliationMaxInterval(); + } + } + + public static T valueOrDefault( + ControllerConfiguration controllerConfiguration, + Function mapper, + T defaultValue) { + if (controllerConfiguration == null) { + return defaultValue; + } else { + return mapper.apply(controllerConfiguration); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public List getDependentResources() { + if (specs == null) { + final var dependents = + valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); + if (dependents.length == 0) { + return Collections.emptyList(); + } + + specs = new ArrayList<>(dependents.length); + for (Dependent dependent : dependents) { + final Class dependentType = dependent.type(); + if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { + final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); + final var namespaces = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::namespaces, + this.getNamespaces().toArray(new String[0])); + final var labelSelector = + Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); + final var addOwnerReference = + Utils.valueOrDefault( + kubeDependent, + KubernetesDependent::addOwnerReference, + KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + KubernetesDependentResourceConfig config = + new KubernetesDependentResourceConfig( + addOwnerReference, namespaces, labelSelector, getConfigurationService()); + specs.add(new DependentResourceSpec(dependentType, config)); + } else { + specs.add(new DependentResourceSpec(dependentType)); + } + } + } + return specs; + } +} 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 894091ad2d..bfc4c6c588 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 @@ -3,6 +3,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; + public class BaseConfigurationService extends AbstractConfigurationService { private static final String LOGGER_NAME = "Default ConfigurationService implementation"; @@ -12,6 +15,10 @@ public BaseConfigurationService(Version version) { super(version); } + public BaseConfigurationService() { + this(Utils.loadFromProperties()); + } + @Override protected void logMissingReconcilerWarning(String reconcilerKey, String reconcilersNameMessage) { logger.warn("Configuration for reconciler '{}' was not found. {}", reconcilerKey, @@ -25,4 +32,41 @@ public String getLoggerName() { protected Logger getLogger() { return logger; } + + @Override + public ControllerConfiguration getConfigurationFor( + Reconciler reconciler) { + var config = super.getConfigurationFor(reconciler); + if (config == null) { + if (createIfNeeded()) { + // create the configuration on demand and register it + config = configFor(reconciler); + register(config); + getLogger().info( + "Created configuration for reconciler {} with name {}", + reconciler.getClass().getName(), + config.getName()); + } + } else { + // check that we don't have a reconciler name collision + final var newControllerClassName = reconciler.getClass().getCanonicalName(); + if (!config.getAssociatedReconcilerClassName().equals(newControllerClassName)) { + throwExceptionOnNameCollision(newControllerClassName, config); + } + } + return config; + } + + protected ControllerConfiguration configFor(Reconciler reconciler) { + return new AnnotationControllerConfiguration<>(reconciler); + } + + protected boolean createIfNeeded() { + return true; + } + + @Override + public boolean checkCRDAndValidateLocalModel() { + return Utils.shouldCheckCRDAndValidateLocalModel(); + } } 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 index 67a8937ac4..e1e5e83fa7 100644 --- 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 @@ -1,183 +1,19 @@ package io.javaoperatorsdk.operator.config.runtime; -import java.time.Duration; import java.util.*; -import java.util.function.Function; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.Utils; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -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.processing.dependent.kubernetes.KubernetesDependent; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; -import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -@SuppressWarnings("rawtypes") public class AnnotationControllerConfiguration - implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { - - private final Reconciler reconciler; - private final ControllerConfiguration annotation; - private ConfigurationService service; - private List specs; + extends io.javaoperatorsdk.operator.api.config.AnnotationControllerConfiguration { public AnnotationControllerConfiguration(Reconciler reconciler) { - this.reconciler = reconciler; - this.annotation = reconciler.getClass().getAnnotation(ControllerConfiguration.class); - } - - @Override - public String getName() { - return ReconcilerUtils.getNameFor(reconciler); - } - - @Override - public String getFinalizer() { - 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); + super(reconciler); } @Override public Class getResourceClass() { return RuntimeControllerMetadata.getResourceClass(reconciler); } - - @Override - public Set getNamespaces() { - return Set.of(valueOrDefault(annotation, ControllerConfiguration::namespaces, new String[] {})); - } - - @Override - public String getLabelSelector() { - return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); - } - - @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public void setConfigurationService(ConfigurationService service) { - this.service = service; - } - - @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 reconciliationMaxInterval() { - if (annotation.reconciliationMaxInterval() != null) { - if (annotation.reconciliationMaxInterval().interval() <= 0) { - return Optional.empty(); - } - return Optional.of( - Duration.of( - annotation.reconciliationMaxInterval().interval(), - annotation.reconciliationMaxInterval().timeUnit().toChronoUnit())); - } else { - return io.javaoperatorsdk.operator.api.config.ControllerConfiguration.super.reconciliationMaxInterval(); - } - } - - public static T valueOrDefault( - ControllerConfiguration controllerConfiguration, - Function mapper, - T defaultValue) { - if (controllerConfiguration == null) { - return defaultValue; - } else { - return mapper.apply(controllerConfiguration); - } - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Override - public List getDependentResources() { - if (specs == null) { - final var dependents = - valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); - if (dependents.length == 0) { - return Collections.emptyList(); - } - - specs = new ArrayList<>(dependents.length); - for (Dependent dependent : dependents) { - final Class dependentType = dependent.type(); - if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) { - final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class); - final var namespaces = - Utils.valueOrDefault( - kubeDependent, - KubernetesDependent::namespaces, - this.getNamespaces().toArray(new String[0])); - final var labelSelector = - Utils.valueOrDefault(kubeDependent, KubernetesDependent::labelSelector, null); - final var addOwnerReference = - Utils.valueOrDefault( - kubeDependent, - KubernetesDependent::addOwnerReference, - KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); - KubernetesDependentResourceConfig config = - new KubernetesDependentResourceConfig( - addOwnerReference, namespaces, labelSelector, getConfigurationService()); - specs.add(new DependentResourceSpec(dependentType, config)); - } else { - specs.add(new DependentResourceSpec(dependentType)); - } - } - } - return specs; - } } 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 21af48cfd1..d34d6fee55 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 @@ -3,52 +3,43 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; public class DefaultConfigurationService extends BaseConfigurationService { private static final DefaultConfigurationService instance = new DefaultConfigurationService(); + private boolean createIfNeeded = super.createIfNeeded(); private DefaultConfigurationService() { - super(Utils.loadFromProperties()); + super(); } public static DefaultConfigurationService instance() { return instance; } - @Override - public ControllerConfiguration getConfigurationFor( - Reconciler reconciler) { - return getConfigurationFor(reconciler, true); - } - ControllerConfiguration getConfigurationFor( Reconciler reconciler, boolean createIfNeeded) { - var config = super.getConfigurationFor(reconciler); - if (config == null) { - if (createIfNeeded) { - // create the configuration on demand and register it - config = new AnnotationControllerConfiguration<>(reconciler); - register(config); - getLogger().info( - "Created configuration for reconciler {} with name {}", - reconciler.getClass().getName(), - config.getName()); - } - } else { - // check that we don't have a reconciler name collision - final var newControllerClassName = reconciler.getClass().getCanonicalName(); - if (!config.getAssociatedReconcilerClassName().equals(newControllerClassName)) { - throwExceptionOnNameCollision(newControllerClassName, config); - } + final var previous = createIfNeeded(); + setCreateIfNeeded(createIfNeeded); + try { + return super.getConfigurationFor(reconciler); + } finally { + setCreateIfNeeded(previous); } - return config; } @Override - public boolean checkCRDAndValidateLocalModel() { - return Utils.shouldCheckCRDAndValidateLocalModel(); + protected boolean createIfNeeded() { + return createIfNeeded; + } + + public void setCreateIfNeeded(boolean createIfNeeded) { + this.createIfNeeded = createIfNeeded; + } + + @Override + protected ControllerConfiguration configFor(Reconciler reconciler) { + return new AnnotationControllerConfiguration<>(reconciler); } } From a3a81efc2a9cccdcc500af8f62eee88b5008e862 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Mar 2022 09:09:43 +0100 Subject: [PATCH 0346/1608] chore(deps): bump maven-compiler-plugin from 3.10.0 to 3.10.1 (#1022) Bumps [maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) from 3.10.0 to 3.10.1. - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.10.0...maven-compiler-plugin-3.10.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index dee39368d9..0e00c4c9ca 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 1.8.3 2.11 - 3.10.0 + 3.10.1 3.0.0-M5 3.3.2 3.2.0 diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index 6cf30996a6..eb7c937e95 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -105,7 +105,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.0 + 3.10.1 diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 7b41dfb4b2..ae046ca991 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -90,7 +90,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.0 + 3.10.1 diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index e79e07ac4f..0a7b7a71cf 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -60,7 +60,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.0 + 3.10.1 From b3583299e86851eebd7b01af853b281665aa43ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Mar 2022 09:11:02 +0100 Subject: [PATCH 0347/1608] fix: renaming mapper primary <-> secondary (#1021) --- .../informer/InformerConfiguration.java | 30 +++++++++---------- .../KubernetesDependentResource.java | 12 ++++---- ...ier.java => PrimaryToSecondaryMapper.java} | 2 +- ...ver.java => SecondaryToPrimaryMapper.java} | 3 +- .../event/source/informer/Mappers.java | 14 ++++----- .../informer/InformerEventSourceTest.java | 10 +++---- .../dependent/SecretDependentResource.java | 6 ++-- .../operator/sample/WebappReconciler.java | 8 ++--- .../WebPageReconcilerDependentResources.java | 4 +-- 9 files changed, 43 insertions(+), 46 deletions(-) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{AssociatedSecondaryResourceIdentifier.java => PrimaryToSecondaryMapper.java} (75%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/{PrimaryResourcesRetriever.java => SecondaryToPrimaryMapper.java} (83%) 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 0d08b65f99..3252c87925 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 @@ -10,8 +10,8 @@ import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; public interface InformerConfiguration @@ -20,13 +20,13 @@ public interface InformerConfiguration extends DefaultResourceConfiguration implements InformerConfiguration { - private final PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; - private final AssociatedSecondaryResourceIdentifier

associatedWith; + private final SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; + private final PrimaryToSecondaryMapper

associatedWith; protected DefaultInformerConfiguration(ConfigurationService service, String labelSelector, Class resourceClass, - PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet, - AssociatedSecondaryResourceIdentifier

associatedWith, + SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet, + PrimaryToSecondaryMapper

associatedWith, Set namespaces) { super(labelSelector, resourceClass, namespaces); setConfigurationService(service); @@ -38,24 +38,24 @@ protected DefaultInformerConfiguration(ConfigurationService service, String labe } - public PrimaryResourcesRetriever getPrimaryResourcesRetriever() { + public SecondaryToPrimaryMapper getPrimaryResourcesRetriever() { return secondaryToPrimaryResourcesIdSet; } - public AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier() { + public PrimaryToSecondaryMapper

getAssociatedResourceIdentifier() { return associatedWith; } } - PrimaryResourcesRetriever getPrimaryResourcesRetriever(); + SecondaryToPrimaryMapper getPrimaryResourcesRetriever(); - AssociatedSecondaryResourceIdentifier

getAssociatedResourceIdentifier(); + PrimaryToSecondaryMapper

getAssociatedResourceIdentifier(); class InformerConfigurationBuilder { - private PrimaryResourcesRetriever secondaryToPrimaryResourcesIdSet; - private AssociatedSecondaryResourceIdentifier

associatedWith; + private SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; + private PrimaryToSecondaryMapper

associatedWith; private Set namespaces; private String labelSelector; private final Class resourceClass; @@ -68,13 +68,13 @@ private InformerConfigurationBuilder(Class resourceClass, } public InformerConfigurationBuilder withPrimaryResourcesRetriever( - PrimaryResourcesRetriever primaryResourcesRetriever) { - this.secondaryToPrimaryResourcesIdSet = primaryResourcesRetriever; + SecondaryToPrimaryMapper secondaryToPrimaryMapper) { + this.secondaryToPrimaryResourcesIdSet = secondaryToPrimaryMapper; return this; } public InformerConfigurationBuilder withAssociatedSecondaryResourceIdentifier( - AssociatedSecondaryResourceIdentifier

associatedWith) { + PrimaryToSecondaryMapper

associatedWith) { this.associatedWith = associatedWith; return this; } 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 c94cec73d8..5ff8bc6947 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 @@ -24,9 +24,9 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; @@ -63,11 +63,11 @@ public void configureWith(KubernetesDependentResourceConfig config) { private void configureWith(ConfigurationService configService, String labelSelector, Set namespaces, boolean addOwnerReference) { final var primaryResourcesRetriever = - (this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever) this + (this instanceof SecondaryToPrimaryMapper) ? (SecondaryToPrimaryMapper) this : Mappers.fromOwnerReference(); - final AssociatedSecondaryResourceIdentifier

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

) this + final PrimaryToSecondaryMapper

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

) this : ResourceID::fromResource; InformerConfiguration ic = InformerConfiguration.from(configService, resourceType()) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java similarity index 75% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java index b402429baa..5e88c39727 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/AssociatedSecondaryResourceIdentifier.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryToSecondaryMapper.java @@ -4,6 +4,6 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; @FunctionalInterface -public interface AssociatedSecondaryResourceIdentifier

{ +public interface PrimaryToSecondaryMapper

{ ResourceID associatedSecondaryID(P primary); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java similarity index 83% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java index 8f01a95bb3..7804ac0f82 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/PrimaryResourcesRetriever.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java @@ -5,7 +5,6 @@ import io.javaoperatorsdk.operator.processing.event.ResourceID; @FunctionalInterface -public interface PrimaryResourcesRetriever { - +public interface SecondaryToPrimaryMapper { Set associatedPrimaryResources(T dependentResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java index 21cd3b162a..1c0150b084 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java @@ -5,38 +5,38 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; public class Mappers { private Mappers() {} - public static PrimaryResourcesRetriever fromAnnotation( + public static SecondaryToPrimaryMapper fromAnnotation( String nameKey) { return fromMetadata(nameKey, null, false); } - public static PrimaryResourcesRetriever fromAnnotation( + public static SecondaryToPrimaryMapper fromAnnotation( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, false); } - public static PrimaryResourcesRetriever fromLabel( + public static SecondaryToPrimaryMapper fromLabel( String nameKey) { return fromMetadata(nameKey, null, true); } - public static PrimaryResourcesRetriever fromLabel( + public static SecondaryToPrimaryMapper fromLabel( String nameKey, String namespaceKey) { return fromMetadata(nameKey, namespaceKey, true); } - public static PrimaryResourcesRetriever fromOwnerReference() { + public static SecondaryToPrimaryMapper fromOwnerReference() { return resource -> ResourceID.fromFirstOwnerReference(resource).map(Set::of) .orElse(Collections.emptySet()); } - private static PrimaryResourcesRetriever fromMetadata( + private static SecondaryToPrimaryMapper fromMetadata( String nameKey, String namespaceKey, boolean isLabel) { return resource -> { final var metadata = resource.getMetadata(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 4caf9b70d2..131454b3d2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -16,7 +16,7 @@ import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.mockito.ArgumentMatchers.any; @@ -50,16 +50,16 @@ void setup() { when(labeledResourceClientMock.runnableInformer(0)).thenReturn(informer); when(informerConfiguration.getPrimaryResourcesRetriever()) - .thenReturn(mock(PrimaryResourcesRetriever.class)); + .thenReturn(mock(SecondaryToPrimaryMapper.class)); informerEventSource = new InformerEventSource<>(informerConfiguration, clientMock); informerEventSource.setTemporalResourceCache(temporaryResourceCacheMock); informerEventSource.setEventHandler(eventHandlerMock); - PrimaryResourcesRetriever primaryResourcesRetriever = mock(PrimaryResourcesRetriever.class); + SecondaryToPrimaryMapper secondaryToPrimaryMapper = mock(SecondaryToPrimaryMapper.class); when(informerConfiguration.getPrimaryResourcesRetriever()) - .thenReturn(primaryResourcesRetriever); - when(primaryResourcesRetriever.associatedPrimaryResources(any())) + .thenReturn(secondaryToPrimaryMapper); + when(secondaryToPrimaryMapper.associatedPrimaryResources(any())) .thenReturn(Set.of(ResourceID.fromResource(testDeployment()))); } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 6c71252bb5..1b26eaf47a 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -11,13 +11,11 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.sample.MySQLSchema; -import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*; - public class SecretDependentResource extends KubernetesDependentResource - implements AssociatedSecondaryResourceIdentifier, Creator { + implements PrimaryToSecondaryMapper, Creator { public static final String SECRET_FORMAT = "%s-secret"; public static final String USERNAME_FORMAT = "%s-user"; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 9e10c39ca8..694ff07d02 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -26,9 +26,9 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -52,7 +52,7 @@ public List prepareEventSources(EventSourceContext context) * customResourceId of the WebApp resource we traverse the cache and identify it based on naming * convention. */ - final PrimaryResourcesRetriever webappsMatchingTomcatName = + final SecondaryToPrimaryMapper webappsMatchingTomcatName = (Tomcat t) -> context.getPrimaryCache() .list(webApp -> webApp.getSpec().getTomcat().equals(t.getMetadata().getName())) .map(ResourceID::fromResource) @@ -61,7 +61,7 @@ public List prepareEventSources(EventSourceContext context) /* * We retrieve the Tomcat instance associated with out Webapp from its spec */ - final AssociatedSecondaryResourceIdentifier tomcatFromWebAppSpec = + final PrimaryToSecondaryMapper tomcatFromWebAppSpec = (Webapp webapp) -> new ResourceID( webapp.getSpec().getTomcat(), webapp.getMetadata().getNamespace()); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index b4f6db1395..08a02e8a2c 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -14,8 +14,8 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier; import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; @@ -163,7 +163,7 @@ public static String serviceName(WebPage webPage) { private class ConfigMapDependentResource extends CrudKubernetesDependentResource implements - AssociatedSecondaryResourceIdentifier { + PrimaryToSecondaryMapper { @Override protected ConfigMap desired(WebPage webPage, Context context) { From cfccc718003a5cebcf5bc07c68ad76fb2767f568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Mar 2022 09:11:19 +0100 Subject: [PATCH 0348/1608] feat: dependent resource inherits namespaces from controller config (#1020) --- .../informer/InformerConfiguration.java | 3 +- .../operator/api/reconciler/Context.java | 7 ++-- .../api/reconciler/DefaultContext.java | 12 +++--- .../api/reconciler/EventSourceContext.java | 22 +++++----- .../operator/api/reconciler/Reconciler.java | 4 +- .../dependent/AbstractDependentResource.java | 16 ++++---- .../api/reconciler/dependent/Creator.java | 2 +- .../api/reconciler/dependent/Deleter.java | 2 +- .../dependent/DependentResource.java | 4 +- .../dependent/DesiredEqualsMatcher.java | 2 +- .../api/reconciler/dependent/Matcher.java | 2 +- .../api/reconciler/dependent/Updater.java | 4 +- .../operator/processing/Controller.java | 41 +++++++++---------- .../dependent/DependentResourceManager.java | 4 +- .../AbstractSimpleDependentResource.java | 8 ++-- .../GenericKubernetesResourceMatcher.java | 5 ++- .../GenericResourceUpdatePreProcessor.java | 3 +- .../KubernetesDependentResource.java | 11 ++--- .../event/ReconciliationDispatcher.java | 4 +- .../GenericKubernetesResourceMatcherTest.java | 5 ++- ...GenericResourceUpdatePreProcessorTest.java | 5 ++- .../event/ReconciliationDispatcherTest.java | 2 +- .../ErrorStatusHandlerTestReconciler.java | 3 +- .../retry/RetryTestCustomReconciler.java | 2 +- .../StandaloneDependentTestReconciler.java | 3 +- .../sample/MySQLSchemaReconciler.java | 2 +- .../dependent/SchemaDependentResource.java | 6 +-- .../operator/sample/TomcatReconciler.java | 2 +- .../operator/sample/WebappReconciler.java | 4 +- .../operator/sample/WebPageReconciler.java | 2 +- .../WebPageReconcilerDependentResources.java | 2 +- 31 files changed, 102 insertions(+), 92 deletions(-) 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 3252c87925..50f21629b8 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 @@ -105,7 +105,8 @@ public InformerConfiguration build() { static InformerConfigurationBuilder from( EventSourceContext

context, Class resourceClass) { - return new InformerConfigurationBuilder<>(resourceClass, context.getConfigurationService()); + return new InformerConfigurationBuilder<>(resourceClass, context.getControllerConfiguration() + .getConfigurationService()); } static InformerConfigurationBuilder from(ConfigurationService configurationService, diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 2f203fa07c..2c3fb6ff1e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -2,10 +2,11 @@ import java.util.Optional; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; -public interface Context { +public interface Context

{ Optional getRetryInfo(); @@ -15,7 +16,7 @@ default Optional getSecondaryResource(Class expectedType) { Optional getSecondaryResource(Class expectedType, String eventSourceName); - ConfigurationService getConfigurationService(); + ControllerConfiguration

getControllerConfiguration(); ManagedDependentResourceContext managedDependentResourceContext(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 33f640789b..96084ffd1f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -3,23 +3,23 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; -public class DefaultContext

implements Context { +public class DefaultContext

implements Context

{ private final RetryInfo retryInfo; private final Controller

controller; private final P primaryResource; - private final ConfigurationService configurationService; + private final ControllerConfiguration

controllerConfiguration; private ManagedDependentResourceContext managedDependentResourceContext; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; - this.configurationService = controller.getConfiguration().getConfigurationService(); + this.controllerConfiguration = controller.getConfiguration(); this.managedDependentResourceContext = new ManagedDependentResourceContext( controller.getDependents()); } @@ -37,8 +37,8 @@ public Optional getSecondaryResource(Class expectedType, String eventS } @Override - public ConfigurationService getConfigurationService() { - return configurationService; + public ControllerConfiguration

getControllerConfiguration() { + return controllerConfiguration; } public ManagedDependentResourceContext managedDependentResourceContext() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java index ba32f493ec..9f49c3b737 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceContext.java @@ -2,7 +2,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceCache; @@ -14,13 +14,14 @@ public class EventSourceContext

{ private final ResourceCache

primaryCache; - private final ConfigurationService configurationService; + private final ControllerConfiguration

controllerConfiguration; private final KubernetesClient client; public EventSourceContext(ResourceCache

primaryCache, - ConfigurationService configurationService, KubernetesClient client) { + ControllerConfiguration

controllerConfiguration, + KubernetesClient client) { this.primaryCache = primaryCache; - this.configurationService = configurationService; + this.controllerConfiguration = controllerConfiguration; this.client = client; } @@ -34,16 +35,13 @@ public ResourceCache

getPrimaryCache() { } /** - * Retrieves the {@link ConfigurationService} associated with the operator. This allows, in - * particular, to lookup global configuration information such as the configured - * {@link io.javaoperatorsdk.operator.api.monitoring.Metrics} or - * {@link io.javaoperatorsdk.operator.api.config.Cloner} implementations, which could be useful to - * event sources. + * Retrieves the {@link ControllerConfiguration} associated with the operator. This allows, in + * particular, to lookup controller and global configuration information such as the configured* * - * @return the {@link ConfigurationService} associated with the operator + * @return the {@link ControllerConfiguration} associated with the operator */ - public ConfigurationService getConfigurationService() { - return configurationService; + public ControllerConfiguration

getControllerConfiguration() { + return controllerConfiguration; } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 2b725afd73..0be4d58ae5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -13,7 +13,7 @@ public interface Reconciler { * @return UpdateControl to manage updates on the custom resource (usually the status) after * reconciliation. */ - UpdateControl reconcile(R resource, Context context); + UpdateControl reconcile(R resource, Context context); /** * Note that this method is used in combination with finalizers. If automatic finalizer handling @@ -37,7 +37,7 @@ public interface Reconciler { * finalizer to indicate that the resource should not be deleted after all, in which case * the controller should restore the resource's state appropriately. */ - default DeleteControl cleanup(R resource, Context context) { + default DeleteControl cleanup(R resource, Context context) { return DeleteControl.defaultDelete(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index af727b66eb..3e4414a009 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -36,7 +36,7 @@ public AbstractDependentResource() { } @Override - public void reconcile(P primary, Context context) { + public void reconcile(P primary, Context

context) { final var creatable = isCreatable(primary, context); final var updatable = isUpdatable(primary, context); if (creatable || updatable) { @@ -67,7 +67,7 @@ public void reconcile(P primary, Context context) { } } - protected void handleCreate(R desired, P primary, Context context) { + protected void handleCreate(R desired, P primary, Context

context) { ResourceID resourceID = ResourceID.fromResource(primary); R created = null; try { @@ -107,7 +107,7 @@ private void prepareEventFiltering(R desired, ResourceID resourceID) { } } - protected void handleUpdate(R actual, R desired, P primary, Context context) { + protected void handleUpdate(R actual, R desired, P primary, Context

context) { ResourceID resourceID = ResourceID.fromResource(primary); R updated = null; try { @@ -131,29 +131,29 @@ private RecentOperationCacheFiller eventSourceAsRecentOperationCacheFiller() } @Override - public void cleanup(P primary, Context context) { + public void cleanup(P primary, Context

context) { if (isDeletable(primary, context)) { deleter.delete(primary, context); } } - protected R desired(P primary, Context context) { + protected R desired(P primary, Context

context) { throw new IllegalStateException( "desired method must be implemented if this DependentResource can be created and/or updated"); } @SuppressWarnings("unused") - protected boolean isCreatable(P primary, Context context) { + protected boolean isCreatable(P primary, Context

context) { return creatable; } @SuppressWarnings("unused") - protected boolean isUpdatable(P primary, Context context) { + protected boolean isUpdatable(P primary, Context

context) { return updatable; } @SuppressWarnings("unused") - protected boolean isDeletable(P primary, Context context) { + protected boolean isDeletable(P primary, Context

context) { return deletable; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java index 223203b2b0..48af8fb61b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java @@ -5,5 +5,5 @@ @FunctionalInterface public interface Creator { - R create(R desired, P primary, Context context); + R create(R desired, P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java index b81ffc0380..0a1bfc1bab 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java @@ -5,5 +5,5 @@ @FunctionalInterface public interface Deleter

{ - void delete(P primary, Context context); + void delete(P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 239ac58d0b..8fc3cea73f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -6,9 +6,9 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; public interface DependentResource { - void reconcile(P primary, Context context); + void reconcile(P primary, Context

context); - default void cleanup(P primary, Context context) {} + default void cleanup(P primary, Context

context) {} Optional getResource(P primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java index 99470bf1d5..2cf8b155ff 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java @@ -12,7 +12,7 @@ public DesiredEqualsMatcher(AbstractDependentResource abstractDependentRes } @Override - public Result match(R actualResource, P primary, Context context) { + public Result match(R actualResource, P primary, Context

context) { var desired = abstractDependentResource.desired(primary, context); return Result.computed(actualResource.equals(desired), desired); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java index f0c5bd7fd9..09e78911e6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java @@ -32,5 +32,5 @@ public Optional computedDesired() { } } - Result match(R actualResource, P primary, Context context); + Result match(R actualResource, P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java index 25e0567781..d49ccc632e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java @@ -5,7 +5,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; public interface Updater { - R update(R actual, R desired, P primary, Context context); + R update(R actual, R desired, P primary, Context

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

context); } 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 a6c1979421..5c432bf95e 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 @@ -36,21 +36,21 @@ @SuppressWarnings({"unchecked"}) @Ignore -public class Controller implements Reconciler, - LifecycleAware, EventSourceInitializer { +public class Controller

implements Reconciler

, + LifecycleAware, EventSourceInitializer

{ private static final Logger log = LoggerFactory.getLogger(Controller.class); - private final Reconciler reconciler; - private final ControllerConfiguration configuration; + private final Reconciler

reconciler; + private final ControllerConfiguration

configuration; private final KubernetesClient kubernetesClient; - private final EventSourceManager eventSourceManager; - private final DependentResourceManager dependents; + private final EventSourceManager

eventSourceManager; + private final DependentResourceManager

dependents; private ConfigurationService configurationService; - public Controller(Reconciler reconciler, - ControllerConfiguration configuration, + public Controller(Reconciler

reconciler, + ControllerConfiguration

configuration, KubernetesClient kubernetesClient) { this.reconciler = reconciler; this.configuration = configuration; @@ -61,7 +61,7 @@ public Controller(Reconciler reconciler, } @Override - public DeleteControl cleanup(R resource, Context context) { + public DeleteControl cleanup(P resource, Context

context) { dependents.cleanup(resource, context); return metrics().timeControllerExecution( @@ -89,7 +89,7 @@ public DeleteControl execute() { } @Override - public UpdateControl reconcile(R resource, Context context) { + public UpdateControl

reconcile(P resource, Context

context) { dependents.reconcile(resource, context); return metrics().timeControllerExecution( @@ -105,7 +105,7 @@ public String controllerName() { } @Override - public String successTypeName(UpdateControl result) { + public String successTypeName(UpdateControl

result) { String successType = "resource"; if (result.isUpdateStatus()) { successType = "status"; @@ -117,7 +117,7 @@ public String successTypeName(UpdateControl result) { } @Override - public UpdateControl execute() { + public UpdateControl

execute() { return reconciler.reconcile(resource, context); } }); @@ -130,13 +130,13 @@ private Metrics metrics() { } @Override - public List prepareEventSources(EventSourceContext context) { + public List prepareEventSources(EventSourceContext

context) { final var dependentSources = dependents.prepareEventSources(context); List sources = new LinkedList<>(dependentSources); // add manually defined event sources if (reconciler instanceof EventSourceInitializer) { - sources.addAll(((EventSourceInitializer) reconciler).prepareEventSources(context)); + sources.addAll(((EventSourceInitializer

) reconciler).prepareEventSources(context)); } return sources; } @@ -164,11 +164,11 @@ public String toString() { return "'" + configuration.getName() + "' Controller"; } - public Reconciler getReconciler() { + public Reconciler

getReconciler() { return reconciler; } - public ControllerConfiguration getConfiguration() { + public ControllerConfiguration

getConfiguration() { return configuration; } @@ -176,7 +176,7 @@ public KubernetesClient getClient() { return kubernetesClient; } - public MixedOperation, Resource> getCRClient() { + public MixedOperation, Resource

> getCRClient() { return kubernetesClient.resources(configuration.getResourceClass()); } @@ -189,7 +189,7 @@ public MixedOperation, Resource> getCRClient() { * @throws OperatorException if a problem occurred during the registration process */ public void start() throws OperatorException { - final Class resClass = configuration.getResourceClass(); + final Class

resClass = configuration.getResourceClass(); final String controllerName = configuration.getName(); final var crdName = configuration.getResourceTypeName(); final var specVersion = "v1"; @@ -215,8 +215,7 @@ public void start() throws OperatorException { } final var context = new EventSourceContext<>( - eventSourceManager.getControllerResourceEventSource(), - configurationService(), kubernetesClient); + eventSourceManager.getControllerResourceEventSource(), configuration, kubernetesClient); prepareEventSources(context).forEach(eventSourceManager::registerEventSource); @@ -273,7 +272,7 @@ private void failOnMissingCurrentNS() { } } - public EventSourceManager getEventSourceManager() { + public EventSourceManager

getEventSourceManager() { return eventSourceManager; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java index 554f019d81..3dd3a2860e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java @@ -63,14 +63,14 @@ public List prepareEventSources(EventSourceContext

context) { } @Override - public UpdateControl

reconcile(P resource, Context context) { + public UpdateControl

reconcile(P resource, Context

context) { initContextIfNeeded(resource, context); dependents.forEach(dependent -> dependent.reconcile(resource, context)); return UpdateControl.noUpdate(); } @Override - public DeleteControl cleanup(P resource, Context context) { + public DeleteControl cleanup(P resource, Context

context) { initContextIfNeeded(resource, context); dependents.forEach(dependent -> dependent.cleanup(resource, context)); return Reconciler.super.cleanup(resource, context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index 564646b217..e7a0edd549 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -43,26 +43,26 @@ public Optional getResource(HasMetadata primaryResource) { public abstract Optional fetchResource(HasMetadata primaryResource); @Override - public void reconcile(P primary, Context context) { + public void reconcile(P primary, Context

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

context) { super.cleanup(primary, context); cache.remove(ResourceID.fromResource(primary)); } @Override - protected void handleCreate(R desired, P primary, Context context) { + protected void handleCreate(R desired, P primary, Context

context) { var res = this.creator.create(desired, primary, context); cache.put(ResourceID.fromResource(primary), res); } @Override - protected void handleUpdate(R actual, R desired, P primary, Context context) { + protected void handleUpdate(R actual, R desired, P primary, Context

context) { var res = updater.update(actual, desired, primary, context); cache.put(ResourceID.fromResource(primary), res); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 25d7c098d5..5bde08b0aa 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -39,8 +39,9 @@ static Matcher matcherFor( } @Override - public Result match(R actualResource, P primary, Context context) { - final var objectMapper = context.getConfigurationService().getObjectMapper(); + public Result match(R actualResource, P primary, Context

context) { + final var objectMapper = + context.getControllerConfiguration().getConfigurationService().getObjectMapper(); final var desired = dependentResource.desired(primary, context); // reflection will be replaced by this: diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java index b33719f64e..ce73a409cd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java @@ -44,7 +44,8 @@ protected void updateClonedActual(R actual, R desired) { } public R replaceSpecOnActual(R actual, R desired, Context context) { - var clonedActual = context.getConfigurationService().getResourceCloner().clone(actual); + var clonedActual = context.getControllerConfiguration().getConfigurationService() + .getResourceCloner().clone(actual); updateClonedActual(clonedActual, desired); return clonedActual; } 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 5ff8bc6947..2bee0edf1b 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 @@ -92,11 +92,11 @@ public void configureWith( this.addOwnerReference = addOwnerReference; } - public R create(R target, P primary, Context context) { + public R create(R target, P primary, Context

context) { return prepare(target, primary, "Creating").create(target); } - public R update(R actual, R target, P primary, Context context) { + public R update(R actual, R target, P primary, Context

context) { var updatedActual = processor.replaceSpecOnActual(actual, target, context); return prepare(target, primary, "Updating").replace(updatedActual); } @@ -105,7 +105,7 @@ public Result match(R actualResource, P primary, Context context) { return matcher.match(actualResource, primary, context); } - public void delete(P primary, Context context) { + public void delete(P primary, Context

context) { if (!addOwnerReference) { var resource = getResource(primary); resource.ifPresent(r -> client.resource(r).delete()); @@ -129,7 +129,8 @@ protected NonNamespaceOperation, Resource> prepa @Override public EventSource initEventSource(EventSourceContext

context) { if (informerEventSource == null) { - configureWith(context.getConfigurationService(), null, null, + configureWith(context.getControllerConfiguration().getConfigurationService(), null, + context.getControllerConfiguration().getNamespaces(), KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); log.warn("Using default configuration for " + resourceType().getSimpleName() + " KubernetesDependentResource, call configureWith to provide configuration"); @@ -159,7 +160,7 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { } @Override - protected R desired(P primary, Context context) { + protected R desired(P primary, Context

context) { return super.desired(primary, context); } 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 14ba12636a..2a14e8a69b 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 @@ -163,11 +163,11 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { return createPostExecutionControl(updatedCustomResource, updateControl); } - private void handleErrorStatusHandler(R resource, Context context, + private void handleErrorStatusHandler(R resource, Context context, RuntimeException e) { if (isErrorStatusHandlerPresent()) { try { - var retryInfo = context.getRetryInfo().orElse(new RetryInfo() { + RetryInfo retryInfo = context.getRetryInfo().orElse(new RetryInfo() { @Override public int getAttemptCount() { return 0; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 3ae424cdfb..4621e4700e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import com.fasterxml.jackson.databind.ObjectMapper; @@ -22,8 +23,10 @@ class GenericKubernetesResourceMatcherTest { private static final Context context = mock(Context.class); static { final var configurationService = mock(ConfigurationService.class); + final var controllerConfiguration = mock(ControllerConfiguration.class); when(configurationService.getObjectMapper()).thenReturn(new ObjectMapper()); - when(context.getConfigurationService()).thenReturn(configurationService); + when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); + when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java index 941738ad7f..6c763d20fe 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; @@ -20,8 +21,10 @@ class GenericResourceUpdatePreProcessorTest { static { final var configurationService = mock(ConfigurationService.class); + final var controllerConfiguration = mock(ControllerConfiguration.class); + when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); when(configurationService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); - when(context.getConfigurationService()).thenReturn(configurationService); + when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); } ResourceUpdatePreProcessor processor = 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 f67909056d..f07d88db1e 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 @@ -296,7 +296,7 @@ public boolean isLastAttempt() { ArgumentCaptor.forClass(Context.class); verify(reconciler, times(1)) .reconcile(any(), contextArgumentCaptor.capture()); - Context context = contextArgumentCaptor.getValue(); + Context context = contextArgumentCaptor.getValue(); final var retryInfo = context.getRetryInfo().get(); assertThat(retryInfo.getAttemptCount()).isEqualTo(2); assertThat(retryInfo.isLastAttempt()).isEqualTo(true); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index 0ce67d30c3..c3b8632e02 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -22,7 +22,8 @@ public class ErrorStatusHandlerTestReconciler @Override public UpdateControl reconcile( - ErrorStatusHandlerTestCustomResource resource, Context context) { + ErrorStatusHandlerTestCustomResource resource, + Context context) { var number = numberOfExecutions.addAndGet(1); var retryAttempt = -1; if (context.getRetryInfo().isPresent()) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index a76b5b0113..8ccd7c2160 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -31,7 +31,7 @@ public RetryTestCustomReconciler(int numberOfExecutionFails) { @Override public UpdateControl reconcile(RetryTestCustomResource resource, - Context context) { + Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 12b7f9506e..aa8689e471 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -85,7 +85,8 @@ private static class DeploymentDependentResource extends Updater { @Override - protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { + protected Deployment desired(StandaloneDependentTestCustomResource primary, + Context context) { Deployment deployment = ReconcilerUtils.loadYaml(Deployment.class, getClass(), "nginx-deployment.yaml"); deployment.getMetadata().setName(primary.getMetadata().getName()); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 140047a3b4..eb66f918ab 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -35,7 +35,7 @@ public MySQLSchemaReconciler() {} @Override - public UpdateControl reconcile(MySQLSchema schema, Context context) { + public UpdateControl reconcile(MySQLSchema schema, Context context) { // we only need to update the status if we just built the schema, i.e. when it's present in the // context Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); 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 f1fbc4b642..f5a5e9ea18 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 @@ -42,12 +42,12 @@ public void configureWith(ResourcePollerConfig config) { } @Override - public Schema desired(MySQLSchema primary, Context context) { + public Schema desired(MySQLSchema primary, Context context) { return new Schema(primary.getMetadata().getName(), primary.getSpec().getEncoding()); } @Override - public Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { + public Schema create(Schema target, MySQLSchema mySQLSchema, Context context) { try (Connection connection = getConnection()) { Secret secret = context.getSecondaryResource(Secret.class).orElseThrow(); var username = decode(secret.getData().get(MYSQL_SECRET_USERNAME)); @@ -70,7 +70,7 @@ private Connection getConnection() throws SQLException { } @Override - public void delete(MySQLSchema primary, Context context) { + public void delete(MySQLSchema primary, Context context) { try (Connection connection = getConnection()) { var userName = primary.getStatus() != null ? primary.getStatus().getUserName() : null; SchemaService.deleteSchemaAndRelatedUser(connection, primary.getMetadata().getName(), diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 416d7f3ee0..dfad4533e3 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -30,7 +30,7 @@ public class TomcatReconciler implements Reconciler { private final Logger log = LoggerFactory.getLogger(getClass()); @Override - public UpdateControl reconcile(Tomcat tomcat, Context context) { + public UpdateControl reconcile(Tomcat tomcat, Context context) { return context.getSecondaryResource(Deployment.class).map(deployment -> { Tomcat updatedTomcat = updateTomcatStatus(tomcat, deployment); log.info( diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 694ff07d02..58d17235ae 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -79,7 +79,7 @@ public List prepareEventSources(EventSourceContext context) * change. */ @Override - public UpdateControl reconcile(Webapp webapp, Context context) { + public UpdateControl reconcile(Webapp webapp, Context context) { if (webapp.getStatus() != null && Objects.equals(webapp.getSpec().getUrl(), webapp.getStatus().getDeployedArtifact())) { return UpdateControl.noUpdate(); @@ -120,7 +120,7 @@ public UpdateControl reconcile(Webapp webapp, Context context) { } @Override - public DeleteControl cleanup(Webapp webapp, Context context) { + public DeleteControl cleanup(Webapp webapp, Context context) { String[] command = new String[] {"rm", "/data/" + webapp.getSpec().getContextPath() + ".war"}; String[] commandStatusInAllPods = executeCommandInAllPods(kubernetesClient, webapp, command); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 90fb61251f..ef0c2f5d17 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -56,7 +56,7 @@ public List prepareEventSources(EventSourceContext context } @Override - public UpdateControl reconcile(WebPage webPage, Context context) { + public UpdateControl reconcile(WebPage webPage, Context context) { log.info("Reconciling web page: {}", webPage); if (webPage.getSpec().getHtml().contains("error")) { throw new ErrorSimulationException("Simulating error"); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index 08a02e8a2c..00a0d50d3b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -128,7 +128,7 @@ protected Class resourceType() { new CrudKubernetesDependentResource<>() { @Override - protected Service desired(WebPage webPage, Context context) { + protected Service desired(WebPage webPage, Context context) { Service service = loadYaml(Service.class, getClass(), "service.yaml"); service.getMetadata().setName(serviceName(webPage)); service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); From dc0b6218aacd2464fa6e65112c1086a8233828c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Mar 2022 09:25:10 +0100 Subject: [PATCH 0349/1608] fix: dependent resource cache iller and filter issue (#1018) --- .../dependent/AbstractDependentResource.java | 38 ++++++++++++------- .../StandaloneDependentResourceIT.java | 6 ++- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 3e4414a009..95b82d50ec 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -14,22 +14,12 @@ 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; - private final boolean filteringEventSource; - private final boolean cachingEventSource; protected Creator creator; protected Updater updater; protected Deleter

deleter; @SuppressWarnings("unchecked") public AbstractDependentResource() { - if (this instanceof EventSourceProvider) { - final var eventSource = ((EventSourceProvider

) this).getEventSource(); - filteringEventSource = eventSource instanceof RecentOperationEventFilter; - cachingEventSource = eventSource instanceof RecentOperationCacheFiller; - } else { - filteringEventSource = false; - cachingEventSource = false; - } creator = creatable ? (Creator) this : null; updater = updatable ? (Updater) this : null; deleter = deletable ? (Deleter

) this : null; @@ -81,27 +71,27 @@ protected void handleCreate(R desired, P primary, Context

context) { } private void cleanupAfterEventFiltering(R desired, ResourceID resourceID, R created) { - if (filteringEventSource) { + if (isFilteringEventSource()) { eventSourceAsRecentOperationEventFilter() .cleanupOnCreateOrUpdateEventFiltering(resourceID, created); } } private void cacheAfterCreate(ResourceID resourceID, R created) { - if (cachingEventSource) { + if (isRecentOperationCacheFiller()) { eventSourceAsRecentOperationCacheFiller().handleRecentResourceCreate(resourceID, created); } } private void cacheAfterUpdate(R actual, ResourceID resourceID, R updated) { - if (cachingEventSource) { + if (isRecentOperationCacheFiller()) { eventSourceAsRecentOperationCacheFiller().handleRecentResourceUpdate(resourceID, updated, actual); } } private void prepareEventFiltering(R desired, ResourceID resourceID) { - if (filteringEventSource) { + if (isFilteringEventSource()) { eventSourceAsRecentOperationEventFilter().prepareForCreateOrUpdateEventFiltering(resourceID, desired); } @@ -130,6 +120,26 @@ private RecentOperationCacheFiller eventSourceAsRecentOperationCacheFiller() return (RecentOperationCacheFiller) ((EventSourceProvider

) this).getEventSource(); } + // this cannot be done in constructor since event source might be initialized later + protected boolean isFilteringEventSource() { + if (this instanceof EventSourceProvider) { + final var eventSource = ((EventSourceProvider

) this).getEventSource(); + return eventSource instanceof RecentOperationEventFilter; + } else { + return false; + } + } + + // this cannot be done in constructor since event source might be initialized later + protected boolean isRecentOperationCacheFiller() { + if (this instanceof EventSourceProvider) { + final var eventSource = ((EventSourceProvider

) this).getEventSource(); + return eventSource instanceof RecentOperationCacheFiller; + } else { + return false; + } + } + @Override public void cleanup(P primary, Context

context) { if (isDeletable(primary, context)) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index d86385f1f1..b7d88159e5 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; @@ -53,8 +54,9 @@ void executeUpdateForTestingCacheUpdateForGetResource() { awaitForDeploymentReadyReplicas(1); - createdCR.getSpec().setReplicaCount(2); - operator.replace(StandaloneDependentTestCustomResource.class, createdCR); + var clonedCr = ConfigurationService.DEFAULT_CLONER.clone(createdCR); + clonedCr.getSpec().setReplicaCount(2); + operator.replace(StandaloneDependentTestCustomResource.class, clonedCr); awaitForDeploymentReadyReplicas(2); assertThat( From bd32e8782c20cd0bf02cc52209abf381d84005dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 11 Mar 2022 10:14:30 +0100 Subject: [PATCH 0350/1608] ci: don't re run failing tests (#1019) --- pom.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pom.xml b/pom.xml index e25e5a1d41..7cc3ca78c6 100644 --- a/pom.xml +++ b/pom.xml @@ -206,9 +206,6 @@ org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} - - 3 - org.apache.maven.plugins From 0c4f0a5cbe59c35cd87df54709124d55704d8ec7 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 11 Mar 2022 12:28:33 +0100 Subject: [PATCH 0351/1608] refactor: getDependentResources return type is more generic (#1023) --- .../config/AnnotationControllerConfiguration.java | 15 ++++++--------- .../api/config/ControllerConfiguration.java | 3 +-- .../config/ControllerConfigurationOverrider.java | 4 ++-- .../config/DefaultControllerConfiguration.java | 6 +++--- .../config/dependent/DependentResourceSpec.java | 4 ---- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index 5fd1f3dbfd..a3241edf04 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -21,14 +21,13 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -@SuppressWarnings("rawtypes") public class AnnotationControllerConfiguration implements io.javaoperatorsdk.operator.api.config.ControllerConfiguration { protected final Reconciler reconciler; private final ControllerConfiguration annotation; private ConfigurationService service; - private List specs; + private List> specs; public AnnotationControllerConfiguration(Reconciler reconciler) { this.reconciler = reconciler; @@ -147,7 +146,7 @@ public static T valueOrDefault( @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public List getDependentResources() { + public List> getDependentResources() { if (specs == null) { final var dependents = valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {}); @@ -155,6 +154,7 @@ public List getDependentResources() { return Collections.emptyList(); } + Object config = null; specs = new ArrayList<>(dependents.length); for (Dependent dependent : dependents) { final Class dependentType = dependent.type(); @@ -172,13 +172,10 @@ public List getDependentResources() { kubeDependent, KubernetesDependent::addOwnerReference, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); - KubernetesDependentResourceConfig config = - new KubernetesDependentResourceConfig( - addOwnerReference, namespaces, labelSelector, getConfigurationService()); - specs.add(new DependentResourceSpec(dependentType, config)); - } else { - specs.add(new DependentResourceSpec(dependentType)); + config = new KubernetesDependentResourceConfig( + addOwnerReference, namespaces, labelSelector, getConfigurationService()); } + specs.add(new DependentResourceSpec(dependentType, config)); } } return specs; 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 d269572ee7..5ebcaf6ed4 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 @@ -12,7 +12,6 @@ import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; -@SuppressWarnings("rawtypes") public interface ControllerConfiguration extends ResourceConfiguration { default String getName() { @@ -52,7 +51,7 @@ default ResourceEventFilter getEventFilter() { return ResourceEventFilters.passthrough(); } - default List getDependentResources() { + default List> getDependentResources() { return Collections.emptyList(); } 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 239265af12..a4d194b876 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 @@ -22,7 +22,7 @@ public class ControllerConfigurationOverrider { private ResourceEventFilter customResourcePredicate; private final ControllerConfiguration original; private Duration reconciliationMaxInterval; - private final List dependentResourceSpecs; + private final List> dependentResourceSpecs; private ControllerConfigurationOverrider(ControllerConfiguration original) { finalizer = original.getFinalizer(); @@ -115,7 +115,7 @@ public void addNewDependentResourceConfig(DependentResourceSpec dependentResourc dependentResourceSpecs.add(dependentResourceSpec); } - private Optional findConfigForDependentResourceClass( + private Optional> findConfigForDependentResourceClass( Class dependentResourceClass) { return dependentResourceSpecs.stream() .filter(dc -> dc.getDependentResourceClass().equals(dependentResourceClass)) 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 index 8ce59dd01e..bba8c1ba12 100644 --- 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 @@ -21,7 +21,7 @@ public class DefaultControllerConfiguration private final boolean generationAware; private final RetryConfiguration retryConfiguration; private final ResourceEventFilter resourceEventFilter; - private final List dependents; + private final List> dependents; private final Duration reconciliationMaxInterval; // NOSONAR constructor is meant to provide all information @@ -38,7 +38,7 @@ public DefaultControllerConfiguration( Class resourceClass, Duration reconciliationMaxInterval, ConfigurationService service, - List dependents) { + List> dependents) { super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; this.name = name; @@ -102,7 +102,7 @@ public ResourceEventFilter getEventFilter() { } @Override - public List getDependentResources() { + public List> getDependentResources() { return dependents; } 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 1c07a5c752..0684c04bef 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 @@ -10,10 +10,6 @@ public class DependentResourceSpec, C> { private final C dependentResourceConfig; - public DependentResourceSpec(Class dependentResourceClass) { - this(dependentResourceClass, null); - } - public DependentResourceSpec(Class dependentResourceClass, C dependentResourceConfig) { this.dependentResourceClass = dependentResourceClass; this.dependentResourceConfig = dependentResourceConfig; From 13a6c8362d201664ae5afe2a523f69b50ef1570b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 11 Mar 2022 16:03:45 +0100 Subject: [PATCH 0352/1608] feat: retrieve ConfigurationService from ConfigurationServiceProvider (#1010) --- .../io/javaoperatorsdk/operator/Operator.java | 43 +++-- .../config/AbstractConfigurationService.java | 3 +- .../AnnotationControllerConfiguration.java | 15 +- .../config/ConfigurationServiceOverrider.java | 29 +-- .../config/ConfigurationServiceProvider.java | 67 +++++++ .../api/config/ControllerConfiguration.java | 4 + .../ControllerConfigurationOverrider.java | 1 - .../DefaultControllerConfiguration.java | 13 +- .../config/DefaultResourceConfiguration.java | 11 -- .../api/config/ExecutorServiceManager.java | 8 +- .../api/config/ResourceConfiguration.java | 12 +- .../informer/InformerConfiguration.java | 35 ++-- .../api/reconciler/DefaultContext.java | 2 +- .../dependent/ResourceUpdatePreProcessor.java | 2 +- .../operator/processing/Controller.java | 28 +-- .../GenericKubernetesResourceMatcher.java | 4 +- .../GenericResourceUpdatePreProcessor.java | 6 +- .../KubernetesDependentResource.java | 14 +- .../KubernetesDependentResourceConfig.java | 16 +- .../processing/event/EventProcessor.java | 18 +- .../event/ReconciliationDispatcher.java | 10 +- .../source/informer/InformerManager.java | 5 +- .../operator/ControllerManagerTest.java | 2 +- .../operator/OperatorTest.java | 55 +++--- .../ConfigurationServiceProviderTest.java | 62 +++++++ .../config/ControllerConfigurationTest.java | 8 - .../operator/processing/ControllerTest.java | 24 ++- .../GenericKubernetesResourceMatcherTest.java | 12 +- ...GenericResourceUpdatePreProcessorTest.java | 9 +- .../event/ReconciliationDispatcherTest.java | 168 +++++++++++------- .../source/CustomResourceSelectorTest.java | 55 ++---- .../event/source/ResourceEventFilterTest.java | 3 +- .../ControllerResourceEventSourceTest.java | 7 +- .../sample/simple/TestCustomResourceSpec.java | 20 +++ .../simple/TestCustomResourceStatus.java | 19 ++ .../junit/AbstractOperatorExtension.java | 5 +- .../runtime/DefaultConfigurationService.java | 8 +- .../operator/ConcurrencyIT.java | 6 +- .../operator/ControllerExecutionIT.java | 6 +- .../CreateUpdateDependentEventFilterIT.java | 2 - .../operator/CustomResourceFilterIT.java | 6 +- .../operator/ErrorStatusHandlerIT.java | 2 - .../operator/EventSourceIT.java | 6 +- .../operator/InformerEventSourceIT.java | 2 - .../KubernetesResourceStatusUpdateIT.java | 6 +- .../operator/MaxIntervalIT.java | 6 +- .../operator/MultiVersionCRDIT.java | 2 - .../ObservedGenerationHandlingIT.java | 6 +- .../io/javaoperatorsdk/operator/RetryIT.java | 2 - .../operator/RetryMaxAttemptIT.java | 2 - .../StandaloneDependentResourceIT.java | 6 +- .../operator/SubResourceUpdateIT.java | 6 +- .../operator/UpdatingResAndSubResIT.java | 6 +- .../DefaultConfigurationServiceTest.java | 13 +- .../operator/sample/MySQLSchemaOperator.java | 6 +- .../sample/MySQLSchemaOperatorE2E.java | 6 +- .../operator/sample/TomcatOperator.java | 3 +- .../operator/sample/TomcatOperatorE2E.java | 3 - .../operator/sample/WebPageOperator.java | 3 +- .../WebPageOperatorDependentResourcesE2E.java | 3 - .../operator/sample/WebPageOperatorE2E.java | 3 - .../sample/PureJavaApplicationRunner.java | 9 +- .../operator/sample/Config.java | 4 +- 63 files changed, 471 insertions(+), 457 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java 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 adc3410405..bcd0d7ade0 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 @@ -14,6 +14,8 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.Version; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; @@ -25,14 +27,33 @@ public class Operator implements LifecycleAware { private static final Logger log = LoggerFactory.getLogger(Operator.class); private final KubernetesClient kubernetesClient; - private final ConfigurationService configurationService; private final ControllerManager controllers = new ControllerManager(); + public Operator() { + this(new DefaultKubernetesClient(), ConfigurationServiceProvider.instance()); + } + + public Operator(KubernetesClient kubernetesClient) { + this(kubernetesClient, ConfigurationServiceProvider.instance()); + } + /** + * @deprecated Use {@link #Operator(Consumer)} instead + */ + @Deprecated public Operator(ConfigurationService configurationService) { this(new DefaultKubernetesClient(), configurationService); } + public Operator(Consumer overrider) { + this(new DefaultKubernetesClient(), overrider); + } + + public Operator(KubernetesClient client, Consumer overrider) { + this(client); + ConfigurationServiceProvider.overrideCurrent(overrider); + } + /** * Note that Operator by default closes the client on stop, this can be changed using * {@link ConfigurationService} @@ -42,7 +63,7 @@ public Operator(ConfigurationService configurationService) { */ public Operator(KubernetesClient kubernetesClient, ConfigurationService configurationService) { this.kubernetesClient = kubernetesClient; - this.configurationService = configurationService; + ConfigurationServiceProvider.set(configurationService); } /** Adds a shutdown hook that automatically calls {@link #stop()} ()} when the app shuts down. */ @@ -54,10 +75,6 @@ public KubernetesClient getKubernetesClient() { return kubernetesClient; } - public ConfigurationService getConfigurationService() { - return configurationService; - } - public List getControllers() { return new ArrayList<>(controllers.controllers.values()); } @@ -70,7 +87,7 @@ public List getControllers() { public void start() { controllers.shouldStart(); - final var version = configurationService.getVersion(); + final var version = ConfigurationServiceProvider.instance().getVersion(); log.info( "Operator SDK {} (commit: {}) built on {} starting...", version.getSdkVersion(), @@ -80,12 +97,13 @@ public void start() { final var clientVersion = Version.clientVersion(); log.info("Client version: {}", clientVersion); - ExecutorServiceManager.init(configurationService); + ExecutorServiceManager.init(); controllers.start(); } @Override public void stop() throws OperatorException { + final var configurationService = ConfigurationServiceProvider.instance(); log.info( "Operator SDK {} is shutting down...", configurationService.getVersion().getSdkVersion()); @@ -107,7 +125,8 @@ public void stop() throws OperatorException { */ public void register(Reconciler reconciler) throws OperatorException { - final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); + final var controllerConfiguration = + ConfigurationServiceProvider.instance().getConfigurationFor(reconciler); register(reconciler, controllerConfiguration); } @@ -132,7 +151,8 @@ public void register(Reconciler reconciler, "Cannot register reconciler with name " + reconciler.getClass().getCanonicalName() + " reconciler named " + ReconcilerUtils.getNameFor(reconciler) + " because its configuration cannot be found.\n" + - " Known reconcilers are: " + configurationService.getKnownReconcilerNames()); + " Known reconcilers are: " + + ConfigurationServiceProvider.instance().getKnownReconcilerNames()); } final var controller = new Controller<>(reconciler, configuration, kubernetesClient); @@ -158,7 +178,8 @@ public void register(Reconciler reconciler, */ public void register(Reconciler reconciler, Consumer> configOverrider) { - final var controllerConfiguration = configurationService.getConfigurationFor(reconciler); + final var controllerConfiguration = + ConfigurationServiceProvider.instance().getConfigurationFor(reconciler); var configToOverride = ControllerConfigurationOverrider.override(controllerConfiguration); configOverrider.accept(configToOverride); register(reconciler, configToOverride.build()); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index 48c6b73f2b..2c9e01551e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -26,6 +26,7 @@ protected void replace(ControllerConfiguration config put(config, false); } + @SuppressWarnings("unchecked") private void put( ControllerConfiguration config, boolean failIfExisting) { final var name = config.getName(); @@ -36,7 +37,6 @@ private void put( } } configurations.put(name, config); - config.setConfigurationService(this); } protected void throwExceptionOnNameCollision( @@ -50,6 +50,7 @@ protected void throwExceptionOnNameCollision( + newReconcilerClassName); } + @SuppressWarnings("unchecked") @Override public ControllerConfiguration getConfigurationFor( Reconciler reconciler) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index a3241edf04..72d34e16a8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -26,7 +26,6 @@ public class AnnotationControllerConfiguration protected final Reconciler reconciler; private final ControllerConfiguration annotation; - private ConfigurationService service; private List> specs; public AnnotationControllerConfiguration(Reconciler reconciler) { @@ -77,16 +76,6 @@ public String getLabelSelector() { return valueOrDefault(annotation, ControllerConfiguration::labelSelector, ""); } - @Override - public ConfigurationService getConfigurationService() { - return service; - } - - @Override - public void setConfigurationService(ConfigurationService service) { - this.service = service; - } - @Override public String getAssociatedReconcilerClassName() { return reconciler.getClass().getCanonicalName(); @@ -172,8 +161,8 @@ public static T valueOrDefault( kubeDependent, KubernetesDependent::addOwnerReference, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); - config = new KubernetesDependentResourceConfig( - addOwnerReference, namespaces, labelSelector, getConfigurationService()); + config = + new KubernetesDependentResourceConfig(addOwnerReference, namespaces, labelSelector); } specs.add(new DependentResourceSpec(dependentType, config)); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java index e2251eea64..9beb583d76 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java @@ -2,12 +2,12 @@ import java.util.Set; import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; -import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.Config; import io.javaoperatorsdk.operator.api.monitoring.Metrics; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +@SuppressWarnings("unused") public class ConfigurationServiceOverrider { private final ConfigurationService original; private Metrics metrics; @@ -19,8 +19,7 @@ public class ConfigurationServiceOverrider { private boolean closeClientOnStop; private ExecutorService executorService = null; - public ConfigurationServiceOverrider( - ConfigurationService original) { + ConfigurationServiceOverrider(ConfigurationService original) { this.original = original; this.clientConfig = original.getClientConfiguration(); this.checkCR = original.checkCRDAndValidateLocalModel(); @@ -73,26 +72,12 @@ public ConfigurationServiceOverrider withExecutorService(ExecutorService executo } public ConfigurationService build() { - return new ConfigurationService() { - @Override - public ControllerConfiguration getConfigurationFor( - Reconciler reconciler) { - ControllerConfiguration controllerConfiguration = - original.getConfigurationFor(reconciler); - controllerConfiguration.setConfigurationService(this); - return controllerConfiguration; - } - + return new BaseConfigurationService(original.getVersion()) { @Override public Set getKnownReconcilerNames() { return original.getKnownReconcilerNames(); } - @Override - public Version getVersion() { - return original.getVersion(); - } - @Override public Config getClientConfiguration() { return clientConfig; @@ -133,12 +118,16 @@ public ExecutorService getExecutorService() { if (executorService != null) { return executorService; } else { - return ConfigurationService.super.getExecutorService(); + return super.getExecutorService(); } } }; } + /** + * @deprecated Use {@link ConfigurationServiceProvider#overrideCurrent(Consumer)} instead + */ + @Deprecated(since = "2.2.0") public static ConfigurationServiceOverrider override(ConfigurationService original) { return new ConfigurationServiceOverrider(original); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java new file mode 100644 index 0000000000..1e6dd660cf --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java @@ -0,0 +1,67 @@ +package io.javaoperatorsdk.operator.api.config; + +import java.util.function.Consumer; + +public class ConfigurationServiceProvider { + static final ConfigurationService DEFAULT = + new BaseConfigurationService(Utils.loadFromProperties()); + private static ConfigurationService instance; + private static ConfigurationService defaultConfigurationService = DEFAULT; + private static boolean alreadyConfigured = false; + + private ConfigurationServiceProvider() {} + + public synchronized static ConfigurationService instance() { + if (instance == null) { + set(defaultConfigurationService); + } + return instance; + } + + public synchronized static void set(ConfigurationService instance) { + set(instance, false); + } + + private static void set(ConfigurationService instance, boolean overriding) { + final var current = ConfigurationServiceProvider.instance; + if (!overriding) { + if (current != null && !current.equals(instance)) { + throw new IllegalStateException( + "A ConfigurationService has already been set and cannot be set again. Current: " + + current.getClass().getCanonicalName()); + } + } else { + if (alreadyConfigured) { + throw new IllegalStateException( + "The ConfigurationService has already been overridden once and cannot be changed again. Current: " + + current.getClass().getCanonicalName()); + } else { + alreadyConfigured = true; + } + } + + ConfigurationServiceProvider.instance = instance; + } + + public synchronized static void overrideCurrent( + Consumer overrider) { + final var toOverride = + new ConfigurationServiceOverrider(ConfigurationServiceProvider.instance()); + overrider.accept(toOverride); + ConfigurationServiceProvider.set(toOverride.build(), true); + } + + public synchronized static void setDefault(ConfigurationService defaultConfigurationService) { + ConfigurationServiceProvider.defaultConfigurationService = defaultConfigurationService; + } + + synchronized static ConfigurationService getDefault() { + return defaultConfigurationService; + } + + public synchronized static void reset() { + defaultConfigurationService = DEFAULT; + instance = null; + alreadyConfigured = false; + } +} 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 5ebcaf6ed4..03485412b6 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 @@ -58,4 +58,8 @@ default ResourceEventFilter getEventFilter() { default Optional reconciliationMaxInterval() { return Optional.of(Duration.ofHours(10L)); } + + default ConfigurationService getConfigurationService() { + return ConfigurationServiceProvider.instance(); + } } 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 a4d194b876..87297b8d49 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 @@ -135,7 +135,6 @@ public ControllerConfiguration build() { customResourcePredicate, original.getResourceClass(), reconciliationMaxInterval, - original.getConfigurationService(), dependentResourceSpecs); } 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 index bba8c1ba12..4545c9885e 100644 --- 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 @@ -10,6 +10,7 @@ import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; +@SuppressWarnings("rawtypes") public class DefaultControllerConfiguration extends DefaultResourceConfiguration implements ControllerConfiguration { @@ -37,7 +38,6 @@ public DefaultControllerConfiguration( ResourceEventFilter resourceEventFilter, Class resourceClass, Duration reconciliationMaxInterval, - ConfigurationService service, List> dependents) { super(labelSelector, resourceClass, namespaces); this.associatedControllerClassName = associatedControllerClassName; @@ -52,7 +52,6 @@ public DefaultControllerConfiguration( : retryConfiguration; this.resourceEventFilter = resourceEventFilter; - setConfigurationService(service); this.dependents = dependents != null ? dependents : Collections.emptyList(); } @@ -86,16 +85,6 @@ public RetryConfiguration getRetryConfiguration() { return retryConfiguration; } - - @Override - public void setConfigurationService(ConfigurationService service) { - if (getConfigurationService() != null) { - throw new IllegalStateException("A ConfigurationService is already associated with '" + name - + "' ControllerConfiguration. Cannot change it once set!"); - } - super.setConfigurationService(service); - } - @Override public ResourceEventFilter getEventFilter() { return resourceEventFilter; 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 d7b5f76815..4959b827ad 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 @@ -12,7 +12,6 @@ public class DefaultResourceConfiguration private final Set namespaces; private final boolean watchAllNamespaces; private final Class resourceClass; - private ConfigurationService service; public DefaultResourceConfiguration(String labelSelector, Class resourceClass, String... namespaces) { @@ -48,18 +47,8 @@ public boolean watchAllNamespaces() { return watchAllNamespaces; } - @Override - public ConfigurationService getConfigurationService() { - return service; - } - @Override public Class getResourceClass() { return resourceClass; } - - @Override - public void setConfigurationService(ConfigurationService service) { - this.service = service; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index 254a539ac3..3442466b5c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -25,11 +25,9 @@ private ExecutorServiceManager(InstrumentedExecutorService executor, this.terminationTimeoutSeconds = terminationTimeoutSeconds; } - public static void init(ConfigurationService configuration) { + public static void init() { if (instance == null) { - if (configuration == null) { - configuration = new BaseConfigurationService(Version.UNKNOWN); - } + final var configuration = ConfigurationServiceProvider.instance(); instance = new ExecutorServiceManager( new InstrumentedExecutorService(configuration.getExecutorService()), configuration.getTerminationTimeoutSeconds()); @@ -56,7 +54,7 @@ public static void stop() { public static ExecutorServiceManager instance() { if (instance == null) { // provide a default configuration if none has been provided by init - init(null); + init(); } return instance; } 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 3ebc3ae546..2d947050e7 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 @@ -63,12 +63,8 @@ static boolean currentNamespaceWatched(Set namespaces) { default Set getEffectiveNamespaces() { var targetNamespaces = getNamespaces(); if (watchCurrentNamespace()) { - final var parent = getConfigurationService(); - if (parent == null) { - throw new IllegalStateException( - "Parent ConfigurationService must be set before calling this method"); - } - String namespace = parent.getClientConfiguration().getNamespace(); + final String namespace = + ConfigurationServiceProvider.instance().getClientConfiguration().getNamespace(); if (namespace == null) { throw new OperatorException( "Couldn't retrieve the currently connected namespace. Make sure it's correctly set in your ~/.kube/config file, using, e.g. 'kubectl config set-context --namespace='"); @@ -77,8 +73,4 @@ default Set getEffectiveNamespaces() { } return targetNamespaces; } - - ConfigurationService getConfigurationService(); - - void setConfigurationService(ConfigurationService service); } 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 50f21629b8..55c8216e96 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 @@ -5,7 +5,6 @@ import java.util.Set; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; @@ -14,6 +13,7 @@ import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; +@SuppressWarnings("rawtypes") public interface InformerConfiguration extends ResourceConfiguration { @@ -23,13 +23,12 @@ class DefaultInformerConfiguration private final SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; private final PrimaryToSecondaryMapper

associatedWith; - protected DefaultInformerConfiguration(ConfigurationService service, String labelSelector, + protected DefaultInformerConfiguration(String labelSelector, Class resourceClass, SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet, PrimaryToSecondaryMapper

associatedWith, Set namespaces) { super(labelSelector, resourceClass, namespaces); - setConfigurationService(service); this.secondaryToPrimaryResourcesIdSet = Objects.requireNonNullElse(secondaryToPrimaryResourcesIdSet, Mappers.fromOwnerReference()); @@ -52,6 +51,7 @@ public PrimaryToSecondaryMapper

getAssociatedResourceIdentifier() { PrimaryToSecondaryMapper

getAssociatedResourceIdentifier(); + @SuppressWarnings("unused") class InformerConfigurationBuilder { private SecondaryToPrimaryMapper secondaryToPrimaryResourcesIdSet; @@ -59,12 +59,9 @@ class InformerConfigurationBuilder private Set namespaces; private String labelSelector; private final Class resourceClass; - private final ConfigurationService configurationService; - private InformerConfigurationBuilder(Class resourceClass, - ConfigurationService configurationService) { + private InformerConfigurationBuilder(Class resourceClass) { this.resourceClass = resourceClass; - this.configurationService = configurationService; } public InformerConfigurationBuilder withPrimaryResourcesRetriever( @@ -97,7 +94,7 @@ public InformerConfigurationBuilder withLabelSelector(String labelSelector } public InformerConfiguration build() { - return new DefaultInformerConfiguration<>(configurationService, labelSelector, resourceClass, + return new DefaultInformerConfiguration<>(labelSelector, resourceClass, secondaryToPrimaryResourcesIdSet, associatedWith, namespaces); } @@ -105,23 +102,21 @@ public InformerConfiguration build() { static InformerConfigurationBuilder from( EventSourceContext

context, Class resourceClass) { - return new InformerConfigurationBuilder<>(resourceClass, context.getControllerConfiguration() - .getConfigurationService()); + return new InformerConfigurationBuilder<>(resourceClass); } - static InformerConfigurationBuilder from(ConfigurationService configurationService, - Class resourceClass) { - return new InformerConfigurationBuilder<>(resourceClass, configurationService); + @SuppressWarnings({"rawtypes", "unchecked"}) + static InformerConfigurationBuilder from(Class resourceClass) { + return new InformerConfigurationBuilder<>(resourceClass); } static InformerConfigurationBuilder from( InformerConfiguration configuration) { - return new InformerConfigurationBuilder(configuration.getResourceClass(), - configuration.getConfigurationService()) - .withNamespaces(configuration.getNamespaces()) - .withLabelSelector(configuration.getLabelSelector()) - .withAssociatedSecondaryResourceIdentifier( - configuration.getAssociatedResourceIdentifier()) - .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); + return new InformerConfigurationBuilder(configuration.getResourceClass()) + .withNamespaces(configuration.getNamespaces()) + .withLabelSelector(configuration.getLabelSelector()) + .withAssociatedSecondaryResourceIdentifier( + configuration.getAssociatedResourceIdentifier()) + .withPrimaryResourcesRetriever(configuration.getPrimaryResourcesRetriever()); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 96084ffd1f..d3a5b7bc18 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -13,7 +13,7 @@ public class DefaultContext

implements Context

{ private final Controller

controller; private final P primaryResource; private final ControllerConfiguration

controllerConfiguration; - private ManagedDependentResourceContext managedDependentResourceContext; + private final ManagedDependentResourceContext managedDependentResourceContext; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java index df2541aae9..55ebcca230 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java @@ -5,5 +5,5 @@ public interface ResourceUpdatePreProcessor { - R replaceSpecOnActual(R actual, R desired, Context context); + R replaceSpecOnActual(R actual, R desired, Context context); } 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 5c432bf95e..d769324be9 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 @@ -16,10 +16,8 @@ import io.javaoperatorsdk.operator.CustomResourceUtils; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.Version; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -47,8 +45,6 @@ public class Controller

implements Reconciler

, private final EventSourceManager

eventSourceManager; private final DependentResourceManager

dependents; - private ConfigurationService configurationService; - public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, KubernetesClient kubernetesClient) { @@ -125,7 +121,7 @@ public UpdateControl

execute() { private Metrics metrics() { - final var metrics = configurationService().getMetrics(); + final var metrics = ConfigurationServiceProvider.instance().getMetrics(); return metrics != null ? metrics : Metrics.NOOP; } @@ -202,7 +198,7 @@ public void start() throws OperatorException { try { // check that the custom resource is known by the cluster if configured that way final CustomResourceDefinition crd; // todo: check proper CRD spec version based on config - if (configurationService().checkCRDAndValidateLocalModel() + if (ConfigurationServiceProvider.instance().checkCRDAndValidateLocalModel() && CustomResource.class.isAssignableFrom(resClass)) { crd = kubernetesClient.apiextensions().v1().customResourceDefinitions().withName(crdName) .get(); @@ -227,23 +223,6 @@ public void start() throws OperatorException { } } - private ConfigurationService configurationService() { - if (configurationService == null) { - configurationService = configuration.getConfigurationService(); - // make sure we always have a default configuration service - if (configurationService == null) { - // we shouldn't need to register the configuration with the default service - configurationService = new BaseConfigurationService(Version.UNKNOWN) { - @Override - public boolean checkCRDAndValidateLocalModel() { - return false; - } - }; - } - } - return configurationService; - } - private void throwMissingCRDException(String crdName, String specVersion, String controllerName) { throw new MissingCRDException( crdName, @@ -282,6 +261,7 @@ public void stop() { } } + @SuppressWarnings("rawtypes") public List getDependents() { return dependents.getDependents(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 5bde08b0aa..0bbe19837e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -5,6 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.zjsonpatch.JsonDiff; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; @@ -40,8 +41,7 @@ static Matcher matcherFor( @Override public Result match(R actualResource, P primary, Context

context) { - final var objectMapper = - context.getControllerConfiguration().getConfigurationService().getObjectMapper(); + final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper(); final var desired = dependentResource.desired(primary, context); // reflection will be replaced by this: diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java index ce73a409cd..0e0624d183 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java @@ -4,6 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; @@ -43,9 +44,8 @@ protected void updateClonedActual(R actual, R desired) { } } - public R replaceSpecOnActual(R actual, R desired, Context context) { - var clonedActual = context.getControllerConfiguration().getConfigurationService() - .getResourceCloner().clone(actual); + public R replaceSpecOnActual(R actual, R desired, Context context) { + var clonedActual = ConfigurationServiceProvider.instance().getResourceCloner().clone(actual); updateClonedActual(clonedActual, desired); return clonedActual; } 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 2bee0edf1b..a471df8c09 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 @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -55,13 +54,12 @@ public KubernetesDependentResource() { @Override public void configureWith(KubernetesDependentResourceConfig config) { - configureWith(config.getConfigurationService(), config.labelSelector(), - Set.of(config.namespaces()), config.addOwnerReference()); + configureWith(config.labelSelector(), Set.of(config.namespaces()), config.addOwnerReference()); } @SuppressWarnings("unchecked") - private void configureWith(ConfigurationService configService, String labelSelector, - Set namespaces, boolean addOwnerReference) { + private void configureWith(String labelSelector, Set namespaces, + boolean addOwnerReference) { final var primaryResourcesRetriever = (this instanceof SecondaryToPrimaryMapper) ? (SecondaryToPrimaryMapper) this : Mappers.fromOwnerReference(); @@ -70,7 +68,7 @@ private void configureWith(ConfigurationService configService, String labelSelec ? (PrimaryToSecondaryMapper

) this : ResourceID::fromResource; InformerConfiguration ic = - InformerConfiguration.from(configService, resourceType()) + InformerConfiguration.from(resourceType()) .withLabelSelector(labelSelector) .withNamespaces(namespaces) .withPrimaryResourcesRetriever(primaryResourcesRetriever) @@ -129,9 +127,7 @@ protected NonNamespaceOperation, Resource> prepa @Override public EventSource initEventSource(EventSourceContext

context) { if (informerEventSource == null) { - configureWith(context.getControllerConfiguration().getConfigurationService(), null, - context.getControllerConfiguration().getNamespaces(), - KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); + configureWith(null, null, KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT); log.warn("Using default configuration for " + resourceType().getSimpleName() + " KubernetesDependentResource, call configureWith to provide configuration"); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java index 4464f3fd7d..3a7bd45025 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResourceConfig.java @@ -1,7 +1,5 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; - import static io.javaoperatorsdk.operator.api.reconciler.Constants.EMPTY_STRING; import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent.ADD_OWNER_REFERENCE_DEFAULT; @@ -10,16 +8,14 @@ public class KubernetesDependentResourceConfig { private boolean addOwnerReference = ADD_OWNER_REFERENCE_DEFAULT; private String[] namespaces = new String[0]; private String labelSelector = EMPTY_STRING; - private ConfigurationService configurationService; public KubernetesDependentResourceConfig() {} public KubernetesDependentResourceConfig(boolean addOwnerReference, String[] namespaces, - String labelSelector, ConfigurationService configurationService) { + String labelSelector) { this.addOwnerReference = addOwnerReference; this.namespaces = namespaces; this.labelSelector = labelSelector; - this.configurationService = configurationService; } public KubernetesDependentResourceConfig setAddOwnerReference( @@ -38,12 +34,6 @@ public KubernetesDependentResourceConfig setLabelSelector(String labelSelector) return this; } - public KubernetesDependentResourceConfig setConfigurationService( - ConfigurationService configurationService) { - this.configurationService = configurationService; - return this; - } - public boolean addOwnerReference() { return addOwnerReference; } @@ -55,8 +45,4 @@ public String[] namespaces() { public String labelSelector() { return labelSelector; } - - public ConfigurationService getConfigurationService() { - return configurationService; - } } 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 7f4cae69a4..e44df906d8 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 @@ -15,6 +15,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; @@ -55,13 +56,7 @@ class EventProcessor implements EventHandler, LifecycleAw new ReconciliationDispatcher<>(eventSourceManager.getController()), GenericRetry.fromConfiguration( eventSourceManager.getController().getConfiguration().getRetryConfiguration()), - eventSourceManager.getController().getConfiguration().getConfigurationService() == null - ? Metrics.NOOP - : eventSourceManager - .getController() - .getConfiguration() - .getConfigurationService() - .getMetrics(), + ConfigurationServiceProvider.instance().getMetrics(), eventSourceManager); } @@ -209,11 +204,10 @@ void eventProcessingFinished( if (eventMarker.deleteEventPresent(resourceID)) { cleanupForDeletedEvent(executionScope.getCustomResourceID()); } else { - postExecutionControl.getUpdatedCustomResource().ifPresent(r -> { - eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate( - ResourceID.fromResource(r), r, - executionScope.getResource()); - }); + postExecutionControl.getUpdatedCustomResource().ifPresent( + r -> eventSourceManager.getControllerResourceEventSource().handleRecentResourceUpdate( + ResourceID.fromResource(r), r, + executionScope.getResource())); if (eventMarker.eventPresent(resourceID)) { submitReconciliationExecution(resourceID); } else { 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 2a14e8a69b..088592b3a4 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 @@ -9,7 +9,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.javaoperatorsdk.operator.api.ObservedGenerationAware; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.BaseControl; import io.javaoperatorsdk.operator.api.reconciler.Context; @@ -124,9 +124,8 @@ private PostExecutionControl handleReconcile( private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { if (isErrorStatusHandlerPresent() || shouldUpdateObservedGenerationAutomatically(resource)) { - final var configurationService = configuration().getConfigurationService(); - return configurationService != null ? configurationService.getResourceCloner().clone(resource) - : ConfigurationService.DEFAULT_CLONER.clone(resource); + final var cloner = ConfigurationServiceProvider.instance().getResourceCloner(); + return cloner.clone(resource); } else { return resource; } @@ -163,6 +162,7 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { return createPostExecutionControl(updatedCustomResource, updateControl); } + @SuppressWarnings("unchecked") private void handleErrorStatusHandler(R resource, Context context, RuntimeException e) { if (isErrorStatusHandlerPresent()) { @@ -301,7 +301,7 @@ private R replace(R resource) { return customResourceFacade.replaceWithLock(resource); } - private ControllerConfiguration configuration() { + ControllerConfiguration configuration() { return controller.getConfiguration(); } 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 33f962e7ed..a50eb34441 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 @@ -17,7 +17,7 @@ import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.Cloner; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; import io.javaoperatorsdk.operator.processing.LifecycleAware; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -41,8 +41,7 @@ public void start() throws OperatorException { void initSources(MixedOperation, Resource> client, C configuration, ResourceEventHandler eventHandler) { - final var service = configuration.getConfigurationService(); - cloner = service == null ? ConfigurationService.DEFAULT_CLONER : service.getResourceCloner(); + cloner = ConfigurationServiceProvider.instance().getResourceCloner(); final var targetNamespaces = configuration.getEffectiveNamespaces(); final var labelSelector = configuration.getLabelSelector(); 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 910492a47f..ca02094d46 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 @@ -63,7 +63,7 @@ private static class TestControllerConfiguration 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); this.controller = controller; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java index 99bd7e64b9..f4de6b2141 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/OperatorTest.java @@ -1,14 +1,16 @@ package io.javaoperatorsdk.operator; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.AbstractConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; @@ -17,27 +19,29 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@SuppressWarnings("rawtypes") class OperatorTest { - private final KubernetesClient kubernetesClient = - MockKubernetesClient.client(FooCustomResource.class); - private final ConfigurationService configurationService = mock(ConfigurationService.class); + private final KubernetesClient kubernetesClient = MockKubernetesClient.client(ConfigMap.class); private final ControllerConfiguration configuration = mock(ControllerConfiguration.class); - private final Operator operator = new Operator(kubernetesClient, configurationService); - private final FooReconciler fooReconciler = FooReconciler.create(); + private final Operator operator = new Operator(kubernetesClient); + private final FooReconciler fooReconciler = new FooReconciler(); + + @BeforeAll + @AfterAll + static void setUpConfigurationServiceProvider() { + ConfigurationServiceProvider.reset(); + } @Test @DisplayName("should register `Reconciler` to Controller") + @SuppressWarnings("unchecked") public void shouldRegisterReconcilerToController() { // given - when(configurationService.getConfigurationFor(fooReconciler)).thenReturn(configuration); - when(configuration.watchAllNamespaces()).thenReturn(true); - when(configuration.getName()).thenReturn("FOO"); - when(configuration.getResourceClass()).thenReturn(FooCustomResource.class); - when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); + when(configuration.getResourceClass()).thenReturn(ConfigMap.class); // when - operator.register(fooReconciler); + operator.register(fooReconciler, configuration); // then assertThat(operator.getControllers().size()).isEqualTo(1); @@ -47,28 +51,17 @@ public void shouldRegisterReconcilerToController() { @Test @DisplayName("should throw `OperationException` when Configuration is null") public void shouldThrowOperatorExceptionWhenConfigurationIsNull() { - Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler)); - } - - private static class FooCustomResource extends CustomResource { - } + // use a ConfigurationService that doesn't automatically create configurations + ConfigurationServiceProvider.reset(); + ConfigurationServiceProvider.set(new AbstractConfigurationService(null)); - private static class FooSpec { - } - - private static class FooStatus { + Assertions.assertThrows(OperatorException.class, () -> operator.register(fooReconciler)); } - private static class FooReconciler implements Reconciler { - - private FooReconciler() {} - - public static FooReconciler create() { - return new FooReconciler(); - } + private static class FooReconciler implements Reconciler { @Override - public UpdateControl reconcile(FooCustomResource resource, Context context) { + public UpdateControl reconcile(ConfigMap resource, Context context) { return UpdateControl.noUpdate(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java new file mode 100644 index 0000000000..f69dd0bc29 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.api.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ConfigurationServiceProviderTest { + @BeforeEach + void resetProvider() { + ConfigurationServiceProvider.reset(); + } + + @Test + void shouldProvideADefaultInstanceIfNoneIsSet() { + final var instance = ConfigurationServiceProvider.instance(); + assertNotNull(instance); + assertTrue(instance instanceof BaseConfigurationService); + } + + @Test + void shouldProvideTheSetDefaultInstanceIfProvided() { + final var defaultConfig = new AbstractConfigurationService(null); + ConfigurationServiceProvider.setDefault(defaultConfig); + assertEquals(defaultConfig, ConfigurationServiceProvider.instance()); + } + + @Test + void shouldProvideTheSetInstanceIfProvided() { + final var config = new AbstractConfigurationService(null); + ConfigurationServiceProvider.set(config); + assertEquals(config, ConfigurationServiceProvider.instance()); + } + + @Test + void shouldBePossibleToOverrideConfigOnce() { + final var config = new AbstractConfigurationService(null); + assertTrue(config.checkCRDAndValidateLocalModel()); + + ConfigurationServiceProvider.set(config); + var instance = ConfigurationServiceProvider.instance(); + assertEquals(config, instance); + + ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(false)); + instance = ConfigurationServiceProvider.instance(); + assertNotEquals(config, instance); + assertFalse(instance.checkCRDAndValidateLocalModel()); + + assertThrows(IllegalStateException.class, + () -> ConfigurationServiceProvider.overrideCurrent(o -> o.withCloseClientOnStop(false))); + } + + @Test + void resetShouldResetAllState() { + shouldBePossibleToOverrideConfigOnce(); + + ConfigurationServiceProvider.reset(); + assertEquals(ConfigurationServiceProvider.DEFAULT, ConfigurationServiceProvider.getDefault()); + + shouldBePossibleToOverrideConfigOnce(); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java index 65c73b6b31..05aa637f1f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java @@ -15,14 +15,6 @@ void getCustomResourceClass() { public String getAssociatedReconcilerClassName() { return null; } - - @Override - public ConfigurationService getConfigurationService() { - return null; - } - - @Override - public void setConfigurationService(ConfigurationService service) {} }; assertEquals(TestCustomResource.class, conf.getResourceClass()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index 2954af5aa4..ba31d6634e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -5,7 +5,7 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.MockKubernetesClient; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -17,16 +17,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@SuppressWarnings("unchecked") class ControllerTest { - @Test void crdShouldNotBeCheckedForNativeResources() { final var client = MockKubernetesClient.client(Secret.class); - final var configurationService = mock(ConfigurationService.class); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(Secret.class); - when(configuration.getConfigurationService()).thenReturn(configurationService); final var controller = new Controller(reconciler, configuration, client); controller.start(); @@ -36,27 +34,27 @@ void crdShouldNotBeCheckedForNativeResources() { @Test void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { final var client = MockKubernetesClient.client(TestCustomResource.class); - final var configurationService = mock(ConfigurationService.class); - when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); - when(configuration.getConfigurationService()).thenReturn(configurationService); - final var controller = new Controller(reconciler, configuration, client); - controller.start(); - verify(client, never()).apiextensions(); + try { + ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(false)); + final var controller = new Controller(reconciler, configuration, client); + controller.start(); + verify(client, never()).apiextensions(); + } finally { + ConfigurationServiceProvider.reset(); + } } @Test void crdShouldBeCheckedForCustomResourcesByDefault() { + ConfigurationServiceProvider.reset(); final var client = MockKubernetesClient.client(TestCustomResource.class); - final var configurationService = mock(ConfigurationService.class); - when(configurationService.checkCRDAndValidateLocalModel()).thenCallRealMethod(); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); - when(configuration.getConfigurationService()).thenReturn(configurationService); final var controller = new Controller(reconciler, configuration, client); // since we're not really connected to a cluster and the CRD wouldn't be deployed anyway, we diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 4621e4700e..1eb7ea1bb4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -2,30 +2,28 @@ import java.util.Optional; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; -import com.fasterxml.jackson.databind.ObjectMapper; - import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@SuppressWarnings({"unchecked", "rawtypes"}) class GenericKubernetesResourceMatcherTest { private static final Context context = mock(Context.class); - static { - final var configurationService = mock(ConfigurationService.class); + + @BeforeAll + static void setUp() { final var controllerConfiguration = mock(ControllerConfiguration.class); - when(configurationService.getObjectMapper()).thenReturn(new ObjectMapper()); - when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java index 6c763d20fe..802be9c3d9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java @@ -2,11 +2,11 @@ import java.util.HashMap; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; @@ -15,15 +15,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@SuppressWarnings("rawtypes") class GenericResourceUpdatePreProcessorTest { private static final Context context = mock(Context.class); - static { - final var configurationService = mock(ConfigurationService.class); + @BeforeAll + static void setUp() { final var controllerConfiguration = mock(ControllerConfiguration.class); - when(controllerConfiguration.getConfigurationService()).thenReturn(configurationService); - when(configurationService.getResourceCloner()).thenReturn(ConfigurationService.DEFAULT_CLONER); when(context.getControllerConfiguration()).thenReturn(controllerConfiguration); } 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 f07d88db1e..5e230d0418 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 @@ -4,7 +4,10 @@ import java.util.ArrayList; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -16,7 +19,7 @@ import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.config.Cloner; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.RetryConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Constants; @@ -35,16 +38,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; +@SuppressWarnings({"unchecked", "rawtypes"}) class ReconciliationDispatcherTest { private static final String DEFAULT_FINALIZER = "javaoperatorsdk.io/finalizer"; @@ -52,26 +57,48 @@ class ReconciliationDispatcherTest { public static final long RECONCILIATION_MAX_INTERVAL = 10L; private TestCustomResource testCustomResource; private ReconciliationDispatcher reconciliationDispatcher; - private final Reconciler reconciler = mock(Reconciler.class, - withSettings().extraInterfaces(ErrorStatusHandler.class)); - private final ConfigurationService configService = mock(ConfigurationService.class); + private TestReconciler reconciler; private final CustomResourceFacade customResourceFacade = mock(ReconciliationDispatcher.CustomResourceFacade.class); - private ControllerConfiguration configuration = mock(ControllerConfiguration.class); + + @BeforeAll + static void classSetup() { + /* + * We need this for mock reconcilers to properly generate the expected UpdateControl: without + * this, calls such as `when(reconciler.reconcile(eq(testCustomResource), + * any())).thenReturn(UpdateControl.updateStatus(testCustomResource))` will return null because + * equals will fail on the two equal but NOT identical TestCustomResources because equals is not + * implemented on TestCustomResourceSpec or TestCustomResourceStatus + */ + ConfigurationServiceProvider.overrideCurrent(overrider -> { + overrider.checkingCRDAndValidateLocalModel(false) + .withResourceCloner(new Cloner() { + @Override + public R clone(R object) { + return object; + } + }); + }); + } + + @AfterAll + static void tearDown() { + ConfigurationServiceProvider.reset(); + } @BeforeEach void setup() { testCustomResource = TestUtils.testCustomResource(); + reconciler = spy(new TestReconciler()); reconciliationDispatcher = init(testCustomResource, reconciler, null, customResourceFacade, true); } private ReconciliationDispatcher init(R customResource, - Reconciler reconciler, ControllerConfiguration configuration, + Reconciler reconciler, ControllerConfiguration configuration, CustomResourceFacade customResourceFacade, boolean useFinalizer) { configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; - ReconciliationDispatcherTest.this.configuration = configuration; final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER; when(configuration.getFinalizer()).thenReturn(finalizer); when(configuration.useFinalizer()).thenCallRealMethod(); @@ -81,25 +108,6 @@ private ReconciliationDispatcher init(R customResourc when(configuration.reconciliationMaxInterval()) .thenReturn(Optional.of(Duration.ofHours(RECONCILIATION_MAX_INTERVAL))); - when(configuration.getConfigurationService()).thenReturn(configService); - - - /* - * We need this for mock reconcilers to properly generate the expected UpdateControl: without - * this, calls such as `when(reconciler.reconcile(eq(testCustomResource), - * any())).thenReturn(UpdateControl.updateStatus(testCustomResource))` will return null because - * equals will fail on the two equal but NOT identical TestCustomResources because equals is not - * implemented on TestCustomResourceSpec or TestCustomResourceStatus - */ - when(configService.getResourceCloner()).thenReturn(new Cloner() { - @Override - - public T clone(T object) { - return object; - } - }); - when(reconciler.cleanup(eq(customResource), any())) - .thenReturn(DeleteControl.defaultDelete()); Controller controller = new Controller<>(reconciler, configuration, MockKubernetesClient.client(customResource.getClass())); controller.start(); @@ -131,8 +139,7 @@ void callCreateOrUpdateOnNewResourceIfFinalizerSet() { void updatesOnlyStatusSubResourceIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateStatus(testCustomResource)); + reconciler.reconcile = (r, c) -> UpdateControl.updateStatus(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -144,8 +151,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { void updatesBothResourceAndStatusIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.updateResourceAndStatus(testCustomResource)); + reconciler.reconcile = (r, c) -> UpdateControl.updateResourceAndStatus(testCustomResource); when(customResourceFacade.replaceWithLock(testCustomResource)).thenReturn(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -219,8 +225,7 @@ void removesDefaultFinalizerOnDeleteIfSet() { void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.cleanup(eq(testCustomResource), any())) - .thenReturn(DeleteControl.noFinalizerRemoval()); + reconciler.cleanup = (r, c) -> DeleteControl.noFinalizerRemoval(); markForDeletion(testCustomResource); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -233,8 +238,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.noUpdate()); + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(customResourceFacade, never()).replaceWithLock(any()); @@ -244,8 +248,8 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { @Test void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { removeFinalizers(testCustomResource); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.noUpdate()); + + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -297,7 +301,7 @@ public boolean isLastAttempt() { verify(reconciler, times(1)) .reconcile(any(), contextArgumentCaptor.capture()); Context context = contextArgumentCaptor.getValue(); - final var retryInfo = context.getRetryInfo().get(); + final var retryInfo = context.getRetryInfo().orElseGet(() -> fail("Missing optional")); assertThat(retryInfo.getAttemptCount()).isEqualTo(2); assertThat(retryInfo.isLastAttempt()).isEqualTo(true); } @@ -306,14 +310,14 @@ public boolean isLastAttempt() { void setReScheduleToPostExecutionControlFromUpdateControl() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn( - UpdateControl.updateStatus(testCustomResource).rescheduleAfter(1000L)); + reconciler.reconcile = + (r, c) -> UpdateControl.updateStatus(testCustomResource).rescheduleAfter(1000L); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); + assertThat(control.getReScheduleDelay().orElseGet(() -> fail("Missing optional"))) + .isEqualTo(1000L); } @Test @@ -321,13 +325,14 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); markForDeletion(testCustomResource); - when(reconciler.cleanup(eq(testCustomResource), any())) - .thenReturn(DeleteControl.noFinalizerRemoval().rescheduleAfter(1, TimeUnit.SECONDS)); + reconciler.cleanup = + (r, c) -> DeleteControl.noFinalizerRemoval().rescheduleAfter(1, TimeUnit.SECONDS); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); - assertThat(control.getReScheduleDelay().get()).isEqualTo(1000L); + assertThat(control.getReScheduleDelay().orElseGet(() -> fail("Missing optional"))) + .isEqualTo(1000L); } @Test @@ -347,8 +352,9 @@ void setObservedGenerationForStatusIfNeeded() { PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); - assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) - .isEqualTo(1L); + assertThat(control.getUpdatedCustomResource().orElseGet(() -> fail("Missing optional")) + .getStatus().getObservedGeneration()) + .isEqualTo(1L); } @Test @@ -367,8 +373,9 @@ void updatesObservedGenerationOnNoUpdateUpdateControl() { PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); - assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) - .isEqualTo(1L); + assertThat(control.getUpdatedCustomResource().orElseGet(() -> fail("Missing optional")) + .getStatus().getObservedGeneration()) + .isEqualTo(1L); } @Test @@ -388,20 +395,22 @@ void updateObservedGenerationOnCustomResourceUpdate() { PostExecutionControl control = dispatcher.handleExecution( executionScopeWithCREvent(observedGenResource)); - assertThat(control.getUpdatedCustomResource().get().getStatus().getObservedGeneration()) - .isEqualTo(1L); + assertThat(control.getUpdatedCustomResource().orElseGet(() -> fail("Missing optional")) + .getStatus().getObservedGeneration()) + .isEqualTo(1L); } @Test void callErrorStatusHandlerIfImplemented() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(any(), any())) - .thenThrow(new IllegalStateException("Error Status Test")); - when(((ErrorStatusHandler) reconciler).updateErrorStatus(any(), any(), any())).then(a -> { + reconciler.reconcile = (r, c) -> { + throw new IllegalStateException("Error Status Test"); + }; + reconciler.errorHandler = (r, ri, e) -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); return Optional.of(testCustomResource); - }); + }; reconciliationDispatcher.handleExecution( new ExecutionScope( @@ -427,12 +436,14 @@ public boolean isLastAttempt() { void callErrorStatusHandlerEvenOnFirstError() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(any(), any())) - .thenThrow(new IllegalStateException("Error Status Test")); - when(((ErrorStatusHandler) reconciler).updateErrorStatus(any(), any(), any())).then(a -> { + reconciler.reconcile = (r, c) -> { + throw new IllegalStateException("Error Status Test"); + }; + reconciler.errorHandler = (r, ri, e) -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); return Optional.of(testCustomResource); - }); + }; + reconciliationDispatcher.handleExecution( new ExecutionScope( testCustomResource, null)); @@ -445,8 +456,7 @@ void callErrorStatusHandlerEvenOnFirstError() { void schedulesReconciliationIfMaxDelayIsSet() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.noUpdate()); + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); PostExecutionControl control = reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -459,9 +469,8 @@ void schedulesReconciliationIfMaxDelayIsSet() { void canSkipSchedulingMaxDelayIf() { testCustomResource.addFinalizer(DEFAULT_FINALIZER); - when(reconciler.reconcile(eq(testCustomResource), any())) - .thenReturn(UpdateControl.noUpdate()); - when(configuration.reconciliationMaxInterval()) + reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); + when(reconciliationDispatcher.configuration().reconciliationMaxInterval()) .thenReturn(Optional.empty()); PostExecutionControl control = @@ -490,4 +499,35 @@ private void removeFinalizers(CustomResource customResource) { public ExecutionScope executionScopeWithCREvent(T resource) { return new ExecutionScope<>(resource, null); } + + private class TestReconciler + implements Reconciler, ErrorStatusHandler { + private BiFunction> reconcile; + private BiFunction cleanup; + private ErrorStatusHandler errorHandler; + + @Override + public UpdateControl reconcile(TestCustomResource resource, + Context context) { + if (reconcile != null && resource.equals(testCustomResource)) { + return reconcile.apply(resource, context); + } + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(TestCustomResource resource, Context context) { + if (cleanup != null && resource.equals(testCustomResource)) { + return cleanup.apply(resource, context); + } + return DeleteControl.defaultDelete(); + } + + @Override + public Optional updateErrorStatus(TestCustomResource resource, + RetryInfo retryInfo, RuntimeException e) { + return errorHandler != null ? errorHandler.updateErrorStatus(resource, retryInfo, e) + : Optional.empty(); + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index 446e654501..e995d3b9c4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -1,7 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source; import java.util.Date; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -9,7 +8,6 @@ import org.awaitility.core.ConditionTimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.internal.util.collections.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,7 +17,10 @@ import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.api.config.DefaultControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Version; +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; @@ -45,14 +46,17 @@ public class CustomResourceSelectorTest { KubernetesClient client; ConfigurationService configurationService; - @SuppressWarnings("unchecked") @BeforeEach void setUpResources() { + // need to reset the provider if we plan on changing the configuration service since it likely + // has already been set in previous tests + ConfigurationServiceProvider.reset(); + configurationService = spy(ConfigurationService.class); when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(false); when(configurationService.getVersion()).thenReturn(new Version("1", "1", new Date())); - when(configurationService.getConfigurationFor(any(MyController.class))).thenReturn( - new MyConfiguration(configurationService, null)); + when(configurationService.getConfigurationFor(any(MyController.class))) + .thenReturn(new MyConfiguration()); } @Test @@ -78,7 +82,7 @@ void resourceWatchedByLabel() { c1err.incrementAndGet(); } }), - new MyConfiguration(configurationService, "app=foo")); + (overrider) -> overrider.settingNamespace(NAMESPACE).withLabelSelector("app=foo")); o1.start(); o2.register( new MyController( @@ -90,7 +94,7 @@ void resourceWatchedByLabel() { c2err.incrementAndGet(); } }), - new MyConfiguration(configurationService, "app=bar")); + (overrider) -> overrider.settingNamespace(NAMESPACE).withLabelSelector("app=bar")); o2.start(); client.resources(TestCustomResource.class).inNamespace(NAMESPACE).create(newMyResource("foo", @@ -127,40 +131,13 @@ public TestCustomResource newMyResource(String app, String namespace) { return resource; } - public static class MyConfiguration - implements - io.javaoperatorsdk.operator.api.config.ControllerConfiguration { - - private final String labelSelector; - private final ConfigurationService service; - - public MyConfiguration(ConfigurationService configurationService, String labelSelector) { - this.labelSelector = labelSelector; - this.service = configurationService; - } - - @Override - public String getLabelSelector() { - return labelSelector; - } - - @Override - public String getAssociatedReconcilerClassName() { - return MyController.class.getCanonicalName(); - } - - @Override - public Set getNamespaces() { - return Sets.newSet(NAMESPACE); - } + public static class MyConfiguration extends DefaultControllerConfiguration { - @Override - public ConfigurationService getConfigurationService() { - return service; + public MyConfiguration() { + super(MyController.class.getCanonicalName(), "mycontroller", null, Constants.NO_FINALIZER, + false, null, + null, null, null, TestCustomResource.class, null, null); } - - @Override - public void setConfigurationService(ConfigurationService service) {} } @ControllerConfiguration(namespaces = NAMESPACE) 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 0dfb2d09d7..1c4c2bcce1 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 @@ -140,7 +140,7 @@ public ControllerConfig(String finalizer, boolean generationAware, eventFilter, customResourceClass, null, - null, null); + null); } } @@ -150,6 +150,7 @@ public TestController(ControllerConfiguration configuration) super(null, configuration, MockKubernetesClient.client(TestCustomResource.class)); } + @SuppressWarnings("unchecked") @Override public EventSourceManager getEventSourceManager() { return mock(EventSourceManager.class); 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 9782948dba..d06cf97fe8 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 @@ -26,7 +26,7 @@ class ControllerResourceEventSourceTest extends public static final String FINALIZER = "finalizer"; - private TestController testController = new TestController(true); + private final TestController testController = new TestController(true); @BeforeEach public void setup() { @@ -109,9 +109,10 @@ public void callsBroadcastsOnResourceEvents() { eq(customResource1)); } + @SuppressWarnings("unchecked") private static class TestController extends Controller { - private EventSourceManager eventSourceManager = + private final EventSourceManager eventSourceManager = mock(EventSourceManager.class); public TestController(boolean generationAware) { @@ -141,7 +142,7 @@ public TestConfiguration(boolean generationAware) { null, TestCustomResource.class, null, - null, null); + null); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceSpec.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceSpec.java index 5fd9f49084..5c23cc4c95 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceSpec.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceSpec.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.sample.simple; +import java.util.Objects; + public class TestCustomResourceSpec { private String configMapName; @@ -46,4 +48,22 @@ public String toString() { + '\'' + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestCustomResourceSpec that = (TestCustomResourceSpec) o; + return Objects.equals(configMapName, that.configMapName) && Objects.equals( + key, that.key) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(configMapName, key, value); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceStatus.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceStatus.java index 620bbaabd8..ab5559d80c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceStatus.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomResourceStatus.java @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.sample.simple; +import java.util.Objects; + public class TestCustomResourceStatus { private String configMapStatus; @@ -16,4 +18,21 @@ public void setConfigMapStatus(String configMapStatus) { public String toString() { return "TestCustomResourceStatus{" + "configMapStatus='" + configMapStatus + '\'' + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestCustomResourceStatus that = (TestCustomResourceStatus) o; + return Objects.equals(configMapStatus, that.configMapStatus); + } + + @Override + public int hashCode() { + return Objects.hash(configMapStatus); + } } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java index 839b550231..2c2a14b14d 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -22,9 +22,8 @@ import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.fabric8.kubernetes.client.utils.Utils; -import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.api.config.Version; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; public abstract class AbstractOperatorExtension implements HasKubernetesClient, BeforeAllCallback, @@ -200,7 +199,7 @@ public static abstract class AbstractBuilder> { protected boolean oneNamespacePerClass; protected AbstractBuilder() { - this.configurationService = new BaseConfigurationService(Version.UNKNOWN); + this.configurationService = ConfigurationServiceProvider.instance(); this.infrastructure = new ArrayList<>(); this.infrastructureTimeout = Duration.ofMinutes(1); 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 d34d6fee55..34e0b3f1d7 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 @@ -2,6 +2,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; 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.reconciler.Reconciler; @@ -10,11 +11,16 @@ public class DefaultConfigurationService extends BaseConfigurationService { private static final DefaultConfigurationService instance = new DefaultConfigurationService(); private boolean createIfNeeded = super.createIfNeeded(); + static { + // register this as the default configuration service + ConfigurationServiceProvider.setDefault(instance); + } + private DefaultConfigurationService() { super(); } - public static DefaultConfigurationService instance() { + static DefaultConfigurationService instance() { return instance; } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java index 12cf3ca4d8..a9fe44191e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ConcurrencyIT.java @@ -10,7 +10,6 @@ import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestReconciler; @@ -28,10 +27,7 @@ class ConcurrencyIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new TestReconciler(true)) - .build(); + OperatorExtension.builder().withReconciler(new TestReconciler(true)).build(); @Test void manyResourcesGetCreatedUpdatedAndDeleted() throws InterruptedException { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index dbd015587e..0a3ed114a1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ConfigMap; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import io.javaoperatorsdk.operator.sample.simple.TestReconciler; @@ -18,10 +17,7 @@ class ControllerExecutionIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new TestReconciler(true)) - .build(); + OperatorExtension.builder().withReconciler(new TestReconciler(true)).build(); @Test void configMapGetsCreatedForTestCustomResource() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java index 57fbc78838..58dc0a541d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CreateUpdateDependentEventFilterIT.java @@ -7,7 +7,6 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestCustomResource; import io.javaoperatorsdk.operator.sample.createupdateeventfilter.CreateUpdateEventFilterTestCustomResourceSpec; @@ -22,7 +21,6 @@ public class CreateUpdateDependentEventFilterIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new CreateUpdateEventFilterTestReconciler()) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java index 70a3f04777..45387d2b97 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CustomResourceFilterIT.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.customfilter.CustomFilteringTestReconciler; import io.javaoperatorsdk.operator.sample.customfilter.CustomFilteringTestResource; @@ -16,10 +15,7 @@ class CustomResourceFilterIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new CustomFilteringTestReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new CustomFilteringTestReconciler()).build(); @Test void doesCustomFiltering() throws InterruptedException { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java index fbf54a1ad1..8b0e7bab11 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ErrorStatusHandlerIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.errorstatushandler.ErrorStatusHandlerTestCustomResource; @@ -23,7 +22,6 @@ class ErrorStatusHandlerIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(reconciler, new GenericRetry().setMaxAttempts(MAX_RETRY_ATTEMPTS).withLinearRetry()) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index c91ebaad43..eea996cb3b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.event.EventSourceTestCustomResource; @@ -19,10 +18,7 @@ class EventSourceIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(EventSourceTestCustomReconciler.class) - .build(); + OperatorExtension.builder().withReconciler(EventSourceTestCustomReconciler.class).build(); @Test void receivingPeriodicEvents() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java index 7ff36bd375..ddd4113c22 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/InformerEventSourceIT.java @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.informereventsource.InformerEventSourceTestCustomResource; @@ -27,7 +26,6 @@ class InformerEventSourceIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new InformerEventSourceTestCustomReconciler()) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java index a2c7378ff5..603dc2ab1a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/KubernetesResourceStatusUpdateIT.java @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.deployment.DeploymentReconciler; @@ -23,10 +22,7 @@ class KubernetesResourceStatusUpdateIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new DeploymentReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new DeploymentReconciler()).build(); @Test void testReconciliationOfNonCustomResourceAndStatusUpdate() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java index f284dbf407..69585340db 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MaxIntervalIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.maxinterval.MaxIntervalTestCustomResource; import io.javaoperatorsdk.operator.sample.maxinterval.MaxIntervalTestReconciler; @@ -17,10 +16,7 @@ class MaxIntervalIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new MaxIntervalTestReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new MaxIntervalTestReconciler()).build(); @Test void reconciliationTriggeredBasedOnMaxInterval() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java index 930fc210b3..a6cf2d5591 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultiVersionCRDIT.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.multiversioncrd.*; @@ -22,7 +21,6 @@ class MultiVersionCRDIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(MultiVersionCRDTestReconciler1.class) .withReconciler(MultiVersionCRDTestReconciler2.class) .build(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java index 5f86336a1c..4c57a977e3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ObservedGenerationHandlingIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenerationTestCustomResource; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenerationTestReconciler; @@ -17,10 +16,7 @@ class ObservedGenerationHandlingIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new ObservedGenerationTestReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new ObservedGenerationTestReconciler()).build(); @Test void testReconciliationOfNonCustomResourceAndStatusUpdate() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index c4ab025ac5..02b7b7af9e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomReconciler; @@ -27,7 +26,6 @@ class RetryIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler( new RetryTestCustomReconciler(NUMBER_FAILED_EXECUTIONS), new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry() diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java index 5ebb05eb0c..56a36cf6f9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryMaxAttemptIT.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; import io.javaoperatorsdk.operator.sample.retry.RetryTestCustomReconciler; @@ -23,7 +22,6 @@ class RetryMaxAttemptIT { @RegisterExtension OperatorExtension operator = OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(reconciler, new GenericRetry().setInitialInterval(RETRY_INTERVAL).withLinearRetry() .setMaxAttempts(MAX_RETRY_ATTEMPTS)) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index b7d88159e5..1d5f7e9d12 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.api.config.ConfigurationService; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResource; import io.javaoperatorsdk.operator.sample.standalonedependent.StandaloneDependentTestCustomResourceSpec; @@ -23,10 +22,7 @@ class StandaloneDependentResourceIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(new StandaloneDependentTestReconciler()) - .build(); + OperatorExtension.builder().withReconciler(new StandaloneDependentTestReconciler()).build(); @Test void dependentResourceManagesDeployment() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 75597643bf..06728063d4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomReconciler; import io.javaoperatorsdk.operator.sample.subresource.SubResourceTestCustomResource; @@ -22,10 +21,7 @@ class SubResourceUpdateIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(SubResourceTestCustomReconciler.class) - .build(); + OperatorExtension.builder().withReconciler(SubResourceTestCustomReconciler.class).build(); @Test void updatesSubResourceStatus() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index 6bad954400..d86ad51d60 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.OperatorExtension; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomReconciler; import io.javaoperatorsdk.operator.sample.doubleupdate.DoubleUpdateTestCustomResource; @@ -20,10 +19,7 @@ class UpdatingResAndSubResIT { @RegisterExtension OperatorExtension operator = - OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) - .withReconciler(DoubleUpdateTestCustomReconciler.class) - .build(); + OperatorExtension.builder().withReconciler(DoubleUpdateTestCustomReconciler.class).build(); @Test void updatesSubResourceStatus() { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index bce957a399..6fd626b94b 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -25,11 +25,10 @@ class DefaultConfigurationServiceTest { public static final String CUSTOM_FINALIZER_NAME = "a.custom/finalizer"; + final DefaultConfigurationService configurationService = DefaultConfigurationService.instance(); @Test void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { - final var configurationService = DefaultConfigurationService.instance(); - final LoggerContext context = LoggerContext.getContext(false); final PatternLayout layout = PatternLayout.createDefaultLayout(context.getConfiguration()); final ListAppender appender = new ListAppender("list", null, layout, false, false); @@ -75,8 +74,7 @@ void attemptingToRetrieveAnUnknownControllerShouldLogWarning() { @Test void returnsValuesFromControllerAnnotationFinalizer() { final var reconciler = new TestCustomReconciler(); - final var configuration = - DefaultConfigurationService.instance().getConfigurationFor(reconciler); + final var configuration = configurationService.getConfigurationFor(reconciler); assertEquals(CustomResource.getCRDName(TestCustomResource.class), configuration.getResourceTypeName()); assertEquals( @@ -89,8 +87,7 @@ void returnsValuesFromControllerAnnotationFinalizer() { @Test void returnCustomerFinalizerNameIfSet() { final var reconciler = new TestCustomFinalizerReconciler(); - final var configuration = - DefaultConfigurationService.instance().getConfigurationFor(reconciler); + final var configuration = configurationService.getConfigurationFor(reconciler); assertEquals(CUSTOM_FINALIZER_NAME, configuration.getFinalizer()); } @@ -99,9 +96,7 @@ void supportsInnerClassCustomResources() { final var reconciler = new TestCustomFinalizerReconciler(); assertDoesNotThrow( () -> { - DefaultConfigurationService.instance() - .getConfigurationFor(reconciler) - .getAssociatedReconcilerClassName(); + configurationService.getConfigurationFor(reconciler).getAssociatedReconcilerClassName(); }); } 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 8abf73f3f6..0a5703a676 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 @@ -14,8 +14,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics; import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig; import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; @@ -31,9 +29,7 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); Operator operator = new Operator(client, - new ConfigurationServiceOverrider(DefaultConfigurationService.instance()) - .withMetrics(new MicrometerMetrics(new LoggingMeterRegistry())) - .build()); + overrider -> overrider.withMetrics(new MicrometerMetrics(new LoggingMeterRegistry()))); MySQLSchemaReconciler schemaReconciler = new MySQLSchemaReconciler(); 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 ed0ff40489..b8e8b0566a 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 @@ -17,7 +17,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.LocalPortForward; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.OperatorExtension; @@ -39,7 +38,7 @@ class MySQLSchemaOperatorE2E { static final String MY_SQL_NS = "mysql"; - private static List infrastructure = new ArrayList<>(); + private final static List infrastructure = new ArrayList<>(); static { infrastructure.add( @@ -60,10 +59,10 @@ boolean isLocal() { } @RegisterExtension + @SuppressWarnings("unchecked") AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler( new MySQLSchemaReconciler(), c -> { @@ -75,7 +74,6 @@ boolean isLocal() { .withInfrastructure(infrastructure) .build() : E2EOperatorExtension.builder() - .withConfigurationService(DefaultConfigurationService.instance()) .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get()) .withInfrastructure(infrastructure) .build(); 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 09a7394e5b..6b894a3348 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 @@ -14,7 +14,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class TomcatOperator { @@ -24,7 +23,7 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); - Operator operator = new Operator(client, DefaultConfigurationService.instance()); + Operator operator = new Operator(client); operator.register(new TomcatReconciler()); operator.register(new WebappReconciler(client)); operator.installShutdownHook(); diff --git a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java index dabcbdfa4b..2f37638655 100644 --- a/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java +++ b/sample-operators/tomcat-operator/src/test/java/io/javaoperatorsdk/operator/sample/TomcatOperatorE2E.java @@ -10,7 +10,6 @@ import io.fabric8.kubernetes.api.model.*; import io.fabric8.kubernetes.client.*; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.InClusterCurl; @@ -43,13 +42,11 @@ boolean isLocal() { @RegisterExtension AbstractOperatorExtension operator = isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new TomcatReconciler()) .withReconciler(new WebappReconciler(client)) .build() : E2EOperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withOperatorDeployment( client.load(new FileInputStream("k8s/operator.yaml")).get()) .build(); 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 0b643462b1..0277da8199 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 @@ -14,7 +14,6 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class WebPageOperator { public static final String WEBPAGE_RECONCILER_ENV = "WEBPAGE_RECONCILER"; @@ -27,7 +26,7 @@ public static void main(String[] args) throws IOException { Config config = new ConfigBuilder().withNamespace(null).build(); KubernetesClient client = new DefaultKubernetesClient(config); - Operator operator = new Operator(client, DefaultConfigurationService.instance()); + Operator operator = new Operator(client); if (WEBPAGE_RECONCILER_ENV_VALUE.equals(System.getenv(WEBPAGE_RECONCILER_ENV))) { operator.register(new WebPageReconciler(client)); } else { diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java index 580a2022c7..3bfdd3f45b 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.OperatorExtension; @@ -19,12 +18,10 @@ public WebPageOperatorDependentResourcesE2E() throws FileNotFoundException {} isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new WebPageReconcilerDependentResources(client)) .build() : E2EOperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get()) .build(); diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java index a096b03965..b7c8dae6af 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorE2E.java @@ -9,7 +9,6 @@ import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; import io.javaoperatorsdk.operator.junit.E2EOperatorExtension; import io.javaoperatorsdk.operator.junit.OperatorExtension; @@ -27,12 +26,10 @@ public WebPageOperatorE2E() throws FileNotFoundException {} isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withReconciler(new WebPageReconciler(client)) .build() : E2EOperatorExtension.builder() .waitForNamespaceDeletion(false) - .withConfigurationService(DefaultConfigurationService.instance()) .withOperatorDeployment(client.load(new FileInputStream("k8s/operator.yaml")).get(), resources -> { Deployment deployment = (Deployment) resources.stream() diff --git a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java index 19d0a471b7..0a3b59d290 100644 --- a/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java +++ b/smoke-test-samples/pure-java/src/main/java/io/javaoperatorsdk/operator/sample/PureJavaApplicationRunner.java @@ -3,18 +3,13 @@ import java.util.concurrent.Executors; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; public class PureJavaApplicationRunner { public static void main(String[] args) { Operator operator = - new Operator( - ConfigurationServiceOverrider.override(DefaultConfigurationService.instance()) - .withExecutorService(Executors.newCachedThreadPool()) - .withConcurrentReconciliationThreads(2) - .build()); + new Operator(overrider -> overrider.withExecutorService(Executors.newCachedThreadPool()) + .withConcurrentReconciliationThreads(2)); operator.register(new CustomServiceReconciler()); operator.start(); } diff --git a/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java b/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java index 07dd4b50c0..7eed511d18 100644 --- a/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java +++ b/smoke-test-samples/spring-boot-plain/src/main/java/io/javaoperatorsdk/operator/sample/Config.java @@ -7,7 +7,6 @@ import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService; @Configuration public class Config { @@ -19,8 +18,9 @@ public CustomServiceReconciler customServiceController() { // Register all controller beans @Bean(initMethod = "start", destroyMethod = "stop") + @SuppressWarnings("rawtypes") public Operator operator(List controllers) { - Operator operator = new Operator(DefaultConfigurationService.instance()); + Operator operator = new Operator(); controllers.forEach(operator::register); return operator; } From a89832e83f6eda9102dbb63eb11eeef762d40f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 14 Mar 2022 09:32:20 +0100 Subject: [PATCH 0353/1608] Docs: Add glossary (#1024) --- docs/_data/sidebar.yml | 2 ++ docs/documentation/glossary.md | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 docs/documentation/glossary.md diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 5bd1f2ce22..0dc51bddec 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -5,6 +5,8 @@ url: /docs/getting-started - title: How to use Samples url: /docs/using-samples + - title: Glossary + url: /docs/glossary - title: Features url: /docs/features - title: Patterns and Best Practices diff --git a/docs/documentation/glossary.md b/docs/documentation/glossary.md new file mode 100644 index 0000000000..3e48bf7efa --- /dev/null +++ b/docs/documentation/glossary.md @@ -0,0 +1,17 @@ +--- +title: Glossary description: Glossary layout: docs permalink: /docs/glossary +--- + +# Glossary + +- Primary Resource - the resource that represents the desired state that the controller is working + to achieve. While this is often a Custom Resource, this can also be a Kubernetes native resource. +- Secondary Resource - any resource that the controller needs to achieve the desired state + represented by the primary resource. These resources can be created, updated, deleted or simply + read depending on the use case. For example, the `Deployment` controller manages `ReplicatSet` + instances when trying to realize the state represented by the `Deployment`. In this scenario, + the `Deployment` is the primary resource while `ReplicaSet` is one of the secondary resources + managed by the `Deployment` controller. +- Dependent Resource - a feature of JOSDK, aimed at making easier to manage secondary resources. A + dependent resource is therefore a secondary resource implemented using this specific JOSDK + feature. \ No newline at end of file From 511a59e2c19a2360576b1e5201f0da4a91da1ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 14 Mar 2022 15:00:40 +0100 Subject: [PATCH 0354/1608] fix: unit tests (#1032) --- .../processing/event/EventProcessorTest.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) 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 a716a49898..0e968e13a7 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 @@ -7,6 +7,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.internal.stubbing.answers.AnswersWithDelay; +import org.mockito.internal.stubbing.answers.Returns; import org.mockito.stubbing.Answer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,14 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class EventProcessorTest { @@ -40,6 +35,7 @@ class EventProcessorTest { public static final int FAKE_CONTROLLER_EXECUTION_DURATION = 250; public static final int SEPARATE_EXECUTION_TIMEOUT = 450; public static final String TEST_NAMESPACE = "default-event-handler-test"; + private ReconciliationDispatcher reconciliationDispatcherMock = mock(ReconciliationDispatcher.class); private EventSourceManager eventSourceManagerMock = mock(EventSourceManager.class); @@ -188,16 +184,19 @@ void scheduleTimedEventIfInstructedByPostExecutionControl() { } @Test - void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() { + void reScheduleOnlyIfNotExecutedEventsReceivedMeanwhile() throws InterruptedException { var testDelay = 10000L; - when(reconciliationDispatcherMock.handleExecution(any())) - .thenReturn(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)); - - eventProcessor.handleEvent(prepareCREvent()); - eventProcessor.handleEvent(prepareCREvent()); - - verify(retryTimerEventSourceMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(0)) - .scheduleOnce(any(), eq(testDelay)); + doAnswer(new AnswersWithDelay(FAKE_CONTROLLER_EXECUTION_DURATION, + new Returns(PostExecutionControl.defaultDispatch().withReSchedule(testDelay)))) + .when(reconciliationDispatcherMock).handleExecution(any()); + var resourceId = new ResourceID("test1", "default"); + eventProcessor.handleEvent(prepareCREvent(resourceId)); + Thread.sleep(FAKE_CONTROLLER_EXECUTION_DURATION / 3); + eventProcessor.handleEvent(prepareCREvent(resourceId)); + + verify(retryTimerEventSourceMock, + after((long) (FAKE_CONTROLLER_EXECUTION_DURATION * 1.5)).times(0)) + .scheduleOnce(any(), eq(testDelay)); } @Test @@ -205,7 +204,7 @@ void doNotFireEventsIfClosing() { eventProcessor.stop(); eventProcessor.handleEvent(prepareCREvent()); - verify(reconciliationDispatcherMock, timeout(50).times(0)).handleExecution(any()); + verify(reconciliationDispatcherMock, after(50).times(0)).handleExecution(any()); } @Test From 261a38c09b47d41ade4c2d132aad5cdc74c32c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 14 Mar 2022 15:00:50 +0100 Subject: [PATCH 0355/1608] fix: flaky integration test (#1029) --- .../operator/SubResourceUpdateIT.java | 15 ++++++++++----- .../SubResourceTestCustomReconciler.java | 7 ++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 06728063d4..85e11c7287 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -19,6 +19,9 @@ class SubResourceUpdateIT { + public static final int WAIT_AFTER_EXECUTION = 500; + public static final int EVENT_RECEIVE_WAIT = 200; + @RegisterExtension OperatorExtension operator = OperatorExtension.builder().withReconciler(SubResourceTestCustomReconciler.class).build(); @@ -30,7 +33,7 @@ void updatesSubResourceStatus() { awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(200); + waitXms(WAIT_AFTER_EXECUTION); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(2); @@ -45,7 +48,7 @@ void updatesSubResourceStatusNoFinalizer() { awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(200); + waitXms(WAIT_AFTER_EXECUTION); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(2); @@ -60,7 +63,7 @@ void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(200); + waitXms(WAIT_AFTER_EXECUTION); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(2); @@ -77,13 +80,15 @@ void updateCustomResourceAfterSubResourceChange() { SubResourceTestCustomResource resource = createTestCustomResource("1"); operator.create(SubResourceTestCustomResource.class, resource); + // waits for the resource to start processing + waitXms(EVENT_RECEIVE_WAIT); resource.getSpec().setValue("new value"); operator.resources(SubResourceTestCustomResource.class).createOrReplace(resource); awaitStatusUpdated(resource.getMetadata().getName()); // wait for sure, there are no more events - waitXms(500); + waitXms(WAIT_AFTER_EXECUTION); // there is no event on status update processed assertThat(TestUtils.getNumberOfExecutions(operator)) .isEqualTo(3); @@ -117,7 +122,7 @@ public SubResourceTestCustomResource createTestCustomResource(String id) { return resource; } - private void waitXms(int x) { + public static void waitXms(int x) { try { Thread.sleep(x); } catch (InterruptedException e) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index 081100beb3..8bfe6dd67d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -12,16 +12,21 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; +import static io.javaoperatorsdk.operator.support.TestUtils.waitXms; + @ControllerConfiguration(generationAwareEventProcessing = false) public class SubResourceTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { + public static final int RECONCILER_MIN_EXEC_TIME = 300; + public static final String FINALIZER_NAME = ReconcilerUtils.getDefaultFinalizerName(SubResourceTestCustomResource.class); private static final Logger log = LoggerFactory.getLogger(SubResourceTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + @Override public UpdateControl reconcile( SubResourceTestCustomResource resource, Context context) { @@ -33,7 +38,7 @@ public UpdateControl reconcile( ensureStatusExists(resource); resource.getStatus().setState(SubResourceTestCustomResourceStatus.State.SUCCESS); - + waitXms(RECONCILER_MIN_EXEC_TIME); return UpdateControl.updateStatus(resource); } From 6c8affcec0033b01041abd35a98fc8f1bd5bcc2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 14 Mar 2022 15:01:02 +0100 Subject: [PATCH 0356/1608] Reconcile api proposal (#1030) --- .../dependent/AbstractDependentResource.java | 27 +++++++------ .../dependent/DependentResource.java | 2 +- .../reconciler/dependent/ReconcileResult.java | 38 +++++++++++++++++++ .../AbstractSimpleDependentResource.java | 11 ++++-- 4 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 95b82d50ec..0a750d836c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -26,25 +26,27 @@ public AbstractDependentResource() { } @Override - public void reconcile(P primary, Context

context) { - final var creatable = isCreatable(primary, context); - final var updatable = isUpdatable(primary, context); - if (creatable || updatable) { - var maybeActual = getResource(primary); + public ReconcileResult reconcile(P primary, Context

context) { + final var isCreatable = isCreatable(primary, context); + final var isUpdatable = isUpdatable(primary, context); + var maybeActual = getResource(primary); + if (isCreatable || isUpdatable) { if (maybeActual.isEmpty()) { - if (creatable) { + if (isCreatable) { var desired = desired(primary, context); log.debug("Creating dependent {} for primary {}", desired, primary); - handleCreate(desired, primary, context); + var createdResource = handleCreate(desired, primary, context); + return ReconcileResult.resourceCreated(createdResource); } } else { final var actual = maybeActual.get(); - if (updatable) { + if (isUpdatable) { final var match = updater.match(actual, primary, context); if (!match.matched()) { final var desired = match.computedDesired().orElse(desired(primary, context)); log.debug("Updating dependent {} for primary {}", desired, primary); - handleUpdate(actual, desired, primary, context); + var updatedResource = handleUpdate(actual, desired, primary, context); + return ReconcileResult.resourceUpdated(updatedResource); } } else { log.debug("Update skipped for dependent {} as it matched the existing one", actual); @@ -55,15 +57,17 @@ public void reconcile(P primary, Context

context) { "Dependent {} is read-only, implement Creator and/or Updater interfaces to modify it", getClass().getSimpleName()); } + return ReconcileResult.noOperation(maybeActual.orElse(null)); } - protected void handleCreate(R desired, P primary, Context

context) { + protected R handleCreate(R desired, P primary, Context

context) { ResourceID resourceID = ResourceID.fromResource(primary); R created = null; try { prepareEventFiltering(desired, resourceID); created = creator.create(desired, primary, context); cacheAfterCreate(resourceID, created); + return created; } catch (RuntimeException e) { cleanupAfterEventFiltering(desired, resourceID, created); throw e; @@ -97,13 +101,14 @@ private void prepareEventFiltering(R desired, ResourceID resourceID) { } } - protected void handleUpdate(R actual, R desired, P primary, Context

context) { + protected R handleUpdate(R actual, R desired, P primary, Context

context) { ResourceID resourceID = ResourceID.fromResource(primary); R updated = null; try { prepareEventFiltering(desired, resourceID); updated = updater.update(actual, desired, primary, context); cacheAfterUpdate(actual, resourceID, updated); + return updated; } catch (RuntimeException e) { cleanupAfterEventFiltering(desired, resourceID, updated); throw e; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 8fc3cea73f..377e4aea22 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -6,7 +6,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; public interface DependentResource { - void reconcile(P primary, Context

context); + ReconcileResult reconcile(P primary, Context

context); default void cleanup(P primary, Context

context) {} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java new file mode 100644 index 0000000000..392b213e81 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java @@ -0,0 +1,38 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.util.Optional; + +public class ReconcileResult { + + private R resource; + private Operation operation; + + public static ReconcileResult resourceCreated(T resource) { + return new ReconcileResult<>(resource, Operation.CREATED); + } + + public static ReconcileResult resourceUpdated(T resource) { + return new ReconcileResult<>(resource, Operation.UPDATED); + } + + public static ReconcileResult noOperation(T resource) { + return new ReconcileResult<>(resource, Operation.NONE); + } + + private ReconcileResult(R resource, Operation operation) { + this.resource = resource; + this.operation = operation; + } + + public Optional getResource() { + return Optional.ofNullable(resource); + } + + public Operation getOperation() { + return operation; + } + + public enum Operation { + CREATED, UPDATED, NONE + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index e7a0edd549..096e4265f6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -7,6 +7,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.DesiredEqualsMatcher; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.ConcurrentHashMapCache; import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; @@ -43,11 +44,11 @@ public Optional getResource(HasMetadata primaryResource) { public abstract Optional fetchResource(HasMetadata primaryResource); @Override - public void reconcile(P primary, Context

context) { + public ReconcileResult reconcile(P primary, Context

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

context) { @@ -56,15 +57,17 @@ public void cleanup(P primary, Context

context) { } @Override - protected void handleCreate(R desired, P primary, Context

context) { + protected R handleCreate(R desired, P primary, Context

context) { var res = this.creator.create(desired, primary, context); cache.put(ResourceID.fromResource(primary), res); + return res; } @Override - protected void handleUpdate(R actual, R desired, P primary, Context

context) { + protected R handleUpdate(R actual, R desired, P primary, Context

context) { var res = updater.update(actual, desired, primary, context); cache.put(ResourceID.fromResource(primary), res); + return res; } public Matcher.Result match(R actualResource, P primary, Context context) { From be327a73aa6f34a5c96250cf2dfc0bb8a80aff25 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 15 Mar 2022 09:59:25 +0000 Subject: [PATCH 0357/1608] Set new SNAPSHOT version into pom files. --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 4 ++-- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index a2308d86e9..f5f6e12daa 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 4262525b45..bf5ee47e20 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 89a631f0ad..aad9fd5048 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index 0b16c01603..fccc558a17 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 0e00c4c9ca..965eb5206d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index eb7c937e95..ca7a9e1f54 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 933395a4fb..d2ad343e99 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index ae046ca991..187c72fb61 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 0a7b7a71cf..86ee11f906 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT webpage @@ -25,7 +25,7 @@ io.javaoperatorsdk operator-framework - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT org.apache.logging.log4j diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 6cf1f8d991..79aabae280 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index 8a9f424049..ca65419c62 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index f56c811cbd..eb511a925c 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index f97e16f347..3a29992b9d 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.1.2-SNAPSHOT + 2.1.3-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From 5f1e5374cc20206250bcad759783bf861a8e2123 Mon Sep 17 00:00:00 2001 From: Andrea Peruffo Date: Tue, 15 Mar 2022 13:29:03 +0000 Subject: [PATCH 0358/1608] More flexible in cluster curl (#1034) --- .../io/javaoperatorsdk/operator/junit/InClusterCurl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java index 927723209c..dd9748383b 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/InClusterCurl.java @@ -21,11 +21,15 @@ public InClusterCurl(https://github.com/KubernetesClient%20client,%20String%20namespace) { } public String checkUrl(String url) { + return checkUrl("-s", "-o", "/dev/null", "-w", "%{http_code}", url); + } + + public String checkUrl(String... args) { String podName = KubernetesResourceUtil.sanitizeName("curl-" + UUID.randomUUID()); try { Pod curlPod = client.run().inNamespace(namespace) .withRunConfig(new RunConfigBuilder() - .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) + .withArgs(args) .withName(podName) .withImage("curlimages/curl:7.78.0") .withRestartPolicy("Never") From dd6e19df45f79b5c184a183b426660a457a70acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 15 Mar 2022 15:44:02 +0100 Subject: [PATCH 0359/1608] docs: adding migration guide skeleton (#1036) * docs: adding migration guide skeleton * docs: bugix --- docs/_data/sidebar.yml | 3 ++- docs/documentation/glossary.md | 5 ++++- docs/documentation/v3-migration.md | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/documentation/v3-migration.md diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 0dc51bddec..3f3f1734b7 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -19,4 +19,5 @@ url: /docs/contributing - title: Migrating from v1 to v2 url: /docs/v2-migration - + - title: Migrating from v2 to v3 + url: /docs/v3-migration diff --git a/docs/documentation/glossary.md b/docs/documentation/glossary.md index 3e48bf7efa..7716b0ed37 100644 --- a/docs/documentation/glossary.md +++ b/docs/documentation/glossary.md @@ -1,5 +1,8 @@ --- -title: Glossary description: Glossary layout: docs permalink: /docs/glossary +title: Glossary +description: Glossary +layout: docs +permalink: /docs/glossary --- # Glossary diff --git a/docs/documentation/v3-migration.md b/docs/documentation/v3-migration.md new file mode 100644 index 0000000000..716d1704d5 --- /dev/null +++ b/docs/documentation/v3-migration.md @@ -0,0 +1,8 @@ +--- +title: Migrating from v2 to v3 +description: Migrating from v2 to v3 +layout: docs +permalink: /docs/v3-migration +--- + +# Migrating from v2 to v3 From 4056ba312d7bd19e47e4846ce4eb14215b9649af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 15 Mar 2022 18:04:05 +0100 Subject: [PATCH 0360/1608] feat: error handler improvements (#1033) * feat: adding context to error handler; using exception instead runtime exception * fix: error handling compilation issue * feat: added error status update control * Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java Co-authored-by: Chris Laprun * fix: improvements on error handling * fix: exception wrapping * fix: addind missing override * fix: unit tests * docs: update Co-authored-by: Chris Laprun --- docs/documentation/features.md | 10 +- .../micrometer/MicrometerMetrics.java | 12 ++- .../operator/OperatorException.java | 4 + .../operator/api/monitoring/Metrics.java | 6 +- .../api/reconciler/DefaultContext.java | 7 +- .../api/reconciler/ErrorStatusHandler.java | 10 +- .../reconciler/ErrorStatusUpdateControl.java | 41 +++++++ .../operator/api/reconciler/Reconciler.java | 2 +- .../operator/processing/Controller.java | 46 ++++---- .../processing/event/EventProcessor.java | 2 +- .../event/PostExecutionControl.java | 8 +- .../event/ReconciliationDispatcher.java | 45 +++++--- .../event/ReconciliationDispatcherTest.java | 101 ++++++++++++------ .../ErrorStatusHandlerTestReconciler.java | 11 +- .../StandaloneDependentTestReconciler.java | 9 +- .../sample/MySQLSchemaReconciler.java | 16 +-- .../operator/sample/WebPageReconciler.java | 6 +- .../WebPageReconcilerDependentResources.java | 8 +- 18 files changed, 230 insertions(+), 114 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 95924e24dd..35deab4664 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -275,9 +275,9 @@ In order to facilitate error reporting Reconciler can implement the following [interface](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java): ```java -public interface ErrorStatusHandler { +public interface ErrorStatusHandler

{ - Optional updateErrorStatus(T resource, RetryInfo retryInfo, RuntimeException e); + ErrorStatusUpdateControl

updateErrorStatus(P resource, Context

context, Exception e); } ``` @@ -294,6 +294,12 @@ will also produce an event, and will result in a reconciliation if the controlle The scope of this feature is only the `reconcile` method of the reconciler, since there should not be updates on custom resource after it is marked for deletion. +Retry can be skipped for the cases of unrecoverable errors: + +```java + ErrorStatusUpdateControl.updateStatus(customResource).withNoRetry(); +``` + ### Correctness and Automatic Retries There is a possibility to turn off the automatic retries. This is not desirable, unless there is a very specific reason. 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 d08f1c7669..25058e198e 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 @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Optional; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.processing.event.Event; @@ -33,7 +34,13 @@ public T timeControllerExecution(ControllerExecution execution) { .publishPercentileHistogram() .register(registry); try { - final var result = timer.record(execution::execute); + final var result = timer.record(() -> { + try { + return execution.execute(); + } catch (Exception e) { + throw new OperatorException(e); + } + }); final var successType = execution.successTypeName(result); registry .counter(execName + ".success", "controller", name, "type", successType) @@ -58,6 +65,7 @@ public void cleanupDoneFor(ResourceID customResourceUid) { incrementCounter(customResourceUid, "events.delete"); } + @Override public void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfoNullable) { Optional retryInfo = Optional.ofNullable(retryInfoNullable); incrementCounter(resourceID, RECONCILIATIONS + "started", @@ -72,7 +80,7 @@ public void finishedReconciliation(ResourceID resourceID) { incrementCounter(resourceID, RECONCILIATIONS + "success"); } - public void failedReconciliation(ResourceID resourceID, RuntimeException exception) { + public void failedReconciliation(ResourceID resourceID, Exception exception) { var cause = exception.getCause(); if (cause == null) { cause = exception; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/OperatorException.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/OperatorException.java index b11ff0a61a..895d643fcb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/OperatorException.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/OperatorException.java @@ -8,6 +8,10 @@ public OperatorException(String message) { super(message); } + public OperatorException(Throwable cause) { + super(cause); + } + public OperatorException(String message, Throwable cause) { super(message, cause); } 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 d7daba49ae..b1be115bc4 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 @@ -13,7 +13,7 @@ default void receivedEvent(Event event) {} default void reconcileCustomResource(ResourceID resourceID, RetryInfo retryInfo) {} - default void failedReconciliation(ResourceID resourceID, RuntimeException exception) {} + default void failedReconciliation(ResourceID resourceID, Exception exception) {} default void cleanupDoneFor(ResourceID customResourceUid) {} @@ -27,10 +27,10 @@ interface ControllerExecution { String successTypeName(T result); - T execute(); + T execute() throws Exception; } - default T timeControllerExecution(ControllerExecution execution) { + default T timeControllerExecution(ControllerExecution execution) throws Exception { return execution.execute(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index d3a5b7bc18..d80be8b3ae 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -9,7 +9,7 @@ public class DefaultContext

implements Context

{ - private final RetryInfo retryInfo; + private RetryInfo retryInfo; private final Controller

controller; private final P primaryResource; private final ControllerConfiguration

controllerConfiguration; @@ -44,4 +44,9 @@ public ControllerConfiguration

getControllerConfiguration() { public ManagedDependentResourceContext managedDependentResourceContext() { return managedDependentResourceContext; } + + public DefaultContext

setRetryInfo(RetryInfo retryInfo) { + this.retryInfo = retryInfo; + return this; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java index 22a16e4ccd..c7bd09a930 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusHandler.java @@ -1,16 +1,14 @@ package io.javaoperatorsdk.operator.api.reconciler; -import java.util.Optional; - import io.fabric8.kubernetes.api.model.HasMetadata; -public interface ErrorStatusHandler { +public interface ErrorStatusHandler

{ /** *

* Reconciler can implement this interface in order to update the status sub-resource in the case * an exception in thrown. In that case - * {@link #updateErrorStatus(HasMetadata, RetryInfo, RuntimeException)} is called automatically. + * {@link #updateErrorStatus(HasMetadata, Context, Exception)} is called automatically. *

* The result of the method call is used to make a status update on the custom resource. This is * always a sub-resource update request, so no update on custom resource itself (like spec of @@ -21,10 +19,10 @@ public interface ErrorStatusHandler { * should not be updates on custom resource after it is marked for deletion. * * @param resource to update the status on - * @param retryInfo the current retry status + * @param context the current context * @param e exception thrown from the reconciler * @return the updated resource */ - Optional updateErrorStatus(T resource, RetryInfo retryInfo, RuntimeException e); + ErrorStatusUpdateControl

updateErrorStatus(P resource, Context

context, Exception e); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java new file mode 100644 index 0000000000..beb8ddc028 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ErrorStatusUpdateControl.java @@ -0,0 +1,41 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public class ErrorStatusUpdateControl

{ + + private final P resource; + private boolean noRetry = false; + + public static ErrorStatusUpdateControl updateStatus(T resource) { + return new ErrorStatusUpdateControl<>(resource); + } + + public static ErrorStatusUpdateControl noStatusUpdate() { + return new ErrorStatusUpdateControl<>(null); + } + + private ErrorStatusUpdateControl(P resource) { + this.resource = resource; + } + + /** + * Instructs the controller to not retry the error. This is useful for non-recoverable errors. + * + * @return ErrorStatusUpdateControl + */ + public ErrorStatusUpdateControl

withNoRetry() { + this.noRetry = true; + return this; + } + + public Optional

getResource() { + return Optional.ofNullable(resource); + } + + public boolean isNoRetry() { + return noRetry; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 0be4d58ae5..446df36ca1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -13,7 +13,7 @@ public interface Reconciler { * @return UpdateControl to manage updates on the custom resource (usually the status) after * reconciliation. */ - UpdateControl reconcile(R resource, Context context); + UpdateControl reconcile(R resource, Context context) throws Exception; /** * Note that this method is used in combination with finalizers. If automatic finalizer handling 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 d769324be9..5fc1b9ede7 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 @@ -60,32 +60,36 @@ public Controller(Reconciler

reconciler, public DeleteControl cleanup(P resource, Context

context) { dependents.cleanup(resource, context); - return metrics().timeControllerExecution( - new ControllerExecution<>() { - @Override - public String name() { - return "cleanup"; - } + try { + return metrics().timeControllerExecution( + new ControllerExecution<>() { + @Override + public String name() { + return "cleanup"; + } - @Override - public String controllerName() { - return configuration.getName(); - } + @Override + public String controllerName() { + return configuration.getName(); + } - @Override - public String successTypeName(DeleteControl deleteControl) { - return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; - } + @Override + public String successTypeName(DeleteControl deleteControl) { + return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; + } - @Override - public DeleteControl execute() { - return reconciler.cleanup(resource, context); - } - }); + @Override + public DeleteControl execute() { + return reconciler.cleanup(resource, context); + } + }); + } catch (Exception e) { + throw new OperatorException(e); + } } @Override - public UpdateControl

reconcile(P resource, Context

context) { + public UpdateControl

reconcile(P resource, Context

context) throws Exception { dependents.reconcile(resource, context); return metrics().timeControllerExecution( @@ -113,7 +117,7 @@ public String successTypeName(UpdateControl

result) { } @Override - public UpdateControl

execute() { + public UpdateControl

execute() throws Exception { return reconciler.reconcile(resource, context); } }); 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 e44df906d8..4e7ffa4660 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 @@ -242,7 +242,7 @@ TimerEventSource retryEventSource() { * according to the retry timing if there was an exception. */ private void handleRetryOnException( - ExecutionScope executionScope, RuntimeException exception) { + ExecutionScope executionScope, Exception exception) { RetryExecution execution = getOrInitRetryExecution(executionScope); var customResourceID = executionScope.getCustomResourceID(); boolean eventPresent = eventMarker.eventPresent(customResourceID); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java index 5d9a623e20..1171a6d03c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/PostExecutionControl.java @@ -8,14 +8,14 @@ final class PostExecutionControl { private final boolean onlyFinalizerHandled; private final R updatedCustomResource; - private final RuntimeException runtimeException; + private final Exception runtimeException; private Long reScheduleDelay = null; private PostExecutionControl( boolean onlyFinalizerHandled, R updatedCustomResource, - RuntimeException runtimeException) { + Exception runtimeException) { this.onlyFinalizerHandled = onlyFinalizerHandled; this.updatedCustomResource = updatedCustomResource; this.runtimeException = runtimeException; @@ -35,7 +35,7 @@ public static PostExecutionControl customResourceUpda } public static PostExecutionControl exceptionDuringExecution( - RuntimeException exception) { + Exception exception) { return new PostExecutionControl<>(false, null, exception); } @@ -60,7 +60,7 @@ public PostExecutionControl withReSchedule(long delay) { return this; } - public Optional getRuntimeException() { + public Optional getRuntimeException() { return Optional.ofNullable(runtimeException); } 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 088592b3a4..180fbc2d28 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 @@ -48,13 +48,14 @@ public ReconciliationDispatcher(Controller controller) { public PostExecutionControl handleExecution(ExecutionScope executionScope) { try { return handleDispatch(executionScope); - } catch (RuntimeException e) { + } catch (Exception e) { log.error("Error during event processing {} failed.", executionScope, e); return PostExecutionControl.exceptionDuringExecution(e); } } - private PostExecutionControl handleDispatch(ExecutionScope executionScope) { + private PostExecutionControl handleDispatch(ExecutionScope executionScope) + throws Exception { R resource = executionScope.getResource(); log.debug("Handling dispatch for resource {}", getName(resource)); @@ -67,7 +68,7 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) return PostExecutionControl.defaultDispatch(); } - Context context = new DefaultContext<>(executionScope.getRetryInfo(), controller, resource); + Context context = new DefaultContext<>(executionScope.getRetryInfo(), controller, resource); if (markedForDeletion) { return handleCleanup(resource, context); } else { @@ -91,7 +92,7 @@ private boolean shouldNotDispatchToDelete(R resource) { } private PostExecutionControl handleReconcile( - ExecutionScope executionScope, R originalResource, Context context) { + ExecutionScope executionScope, R originalResource, Context context) throws Exception { if (configuration().useFinalizer() && !originalResource.hasFinalizer(configuration().getFinalizer())) { /* @@ -105,11 +106,10 @@ private PostExecutionControl handleReconcile( } else { try { var resourceForExecution = - cloneResourceForErrorStatusHandlerIfNeeded(originalResource, context); + cloneResourceForErrorStatusHandlerIfNeeded(originalResource); return reconcileExecution(executionScope, resourceForExecution, originalResource, context); - } catch (RuntimeException e) { - handleErrorStatusHandler(originalResource, context, e); - throw e; + } catch (Exception e) { + return handleErrorStatusHandler(originalResource, context, e); } } } @@ -121,7 +121,7 @@ private PostExecutionControl handleReconcile( * resource is changed during an execution, and it's much cleaner to have to original resource in * place for status update. */ - private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context) { + private R cloneResourceForErrorStatusHandlerIfNeeded(R resource) { if (isErrorStatusHandlerPresent() || shouldUpdateObservedGenerationAutomatically(resource)) { final var cloner = ConfigurationServiceProvider.instance().getResourceCloner(); @@ -132,7 +132,7 @@ private R cloneResourceForErrorStatusHandlerIfNeeded(R resource, Context context } private PostExecutionControl reconcileExecution(ExecutionScope executionScope, - R resourceForExecution, R originalResource, Context context) { + R resourceForExecution, R originalResource, Context context) throws Exception { log.debug( "Reconciling resource {} with version: {} with execution scope: {}", getName(resourceForExecution), @@ -163,8 +163,8 @@ && shouldUpdateObservedGenerationAutomatically(resourceForExecution)) { } @SuppressWarnings("unchecked") - private void handleErrorStatusHandler(R resource, Context context, - RuntimeException e) { + private PostExecutionControl handleErrorStatusHandler(R resource, Context context, + Exception e) throws Exception { if (isErrorStatusHandlerPresent()) { try { RetryInfo retryInfo = context.getRetryInfo().orElse(new RetryInfo() { @@ -178,13 +178,28 @@ public boolean isLastAttempt() { return controller.getConfiguration().getRetryConfiguration() == null; } }); - var updatedResource = ((ErrorStatusHandler) controller.getReconciler()) - .updateErrorStatus(resource, retryInfo, e); - updatedResource.ifPresent(customResourceFacade::updateStatus); + ((DefaultContext) context).setRetryInfo(retryInfo); + var errorStatusUpdateControl = ((ErrorStatusHandler) controller.getReconciler()) + .updateErrorStatus(resource, context, e); + + R updatedResource = null; + if (errorStatusUpdateControl.getResource().isPresent()) { + updatedResource = + customResourceFacade + .updateStatus(errorStatusUpdateControl.getResource().orElseThrow()); + } + if (errorStatusUpdateControl.isNoRetry()) { + if (updatedResource != null) { + return PostExecutionControl.customResourceUpdated(updatedResource); + } else { + return PostExecutionControl.defaultDispatch(); + } + } } catch (RuntimeException ex) { log.error("Error during error status handling.", ex); } } + throw e; } private boolean isErrorStatusHandlerPresent() { 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 5e230d0418..2e6ee4aa0d 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,13 +22,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.RetryConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Constants; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.CustomResourceFacade; import io.javaoperatorsdk.operator.sample.observedgeneration.ObservedGenCustomResource; @@ -116,7 +110,7 @@ private ReconciliationDispatcher init(R customResourc } @Test - void addFinalizerOnNewResource() { + void addFinalizerOnNewResource() throws Exception { assertFalse(testCustomResource.hasFinalizer(DEFAULT_FINALIZER)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, never()) @@ -128,7 +122,7 @@ void addFinalizerOnNewResource() { } @Test - void callCreateOrUpdateOnNewResourceIfFinalizerSet() { + void callCreateOrUpdateOnNewResourceIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); verify(reconciler, times(1)) @@ -136,7 +130,7 @@ void callCreateOrUpdateOnNewResourceIfFinalizerSet() { } @Test - void updatesOnlyStatusSubResourceIfFinalizerSet() { + void updatesOnlyStatusSubResourceIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.updateStatus(testCustomResource); @@ -148,7 +142,7 @@ void updatesOnlyStatusSubResourceIfFinalizerSet() { } @Test - void updatesBothResourceAndStatusIfFinalizerSet() { + void updatesBothResourceAndStatusIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.updateResourceAndStatus(testCustomResource); @@ -161,7 +155,7 @@ void updatesBothResourceAndStatusIfFinalizerSet() { } @Test - void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() { + void callCreateOrUpdateOnModifiedResourceIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -235,7 +229,7 @@ void doesNotRemovesTheSetFinalizerIfTheDeleteNotMethodInstructsIt() { } @Test - void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { + void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -246,7 +240,7 @@ void doesNotUpdateTheResourceIfNoUpdateUpdateControlIfFinalizerSet() { } @Test - void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() { + void addsFinalizerIfNotMarkedForDeletionAndEmptyCustomResourceReturned() throws Exception { removeFinalizers(testCustomResource); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -269,7 +263,8 @@ void doesNotCallDeleteIfMarkedForDeletionButNotOurFinalizer() { } @Test - void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() { + void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet() + throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); reconciliationDispatcher.handleExecution(executionScopeWithCREvent(testCustomResource)); @@ -278,7 +273,7 @@ void executeControllerRegardlessGenerationInNonGenerationAwareModeIfFinalizerSet } @Test - void propagatesRetryInfoToContextIfFinalizerSet() { + void propagatesRetryInfoToContextIfFinalizerSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciliationDispatcher.handleExecution( @@ -307,7 +302,7 @@ public boolean isLastAttempt() { } @Test - void setReScheduleToPostExecutionControlFromUpdateControl() { + void setReScheduleToPostExecutionControlFromUpdateControl() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = @@ -336,7 +331,7 @@ void reScheduleOnDeleteWithoutFinalizerRemoval() { } @Test - void setObservedGenerationForStatusIfNeeded() { + void setObservedGenerationForStatusIfNeeded() throws Exception { var observedGenResource = createObservedGenCustomResource(); Reconciler reconciler = mock(Reconciler.class); @@ -358,7 +353,7 @@ void setObservedGenerationForStatusIfNeeded() { } @Test - void updatesObservedGenerationOnNoUpdateUpdateControl() { + void updatesObservedGenerationOnNoUpdateUpdateControl() throws Exception { var observedGenResource = createObservedGenCustomResource(); Reconciler reconciler = mock(Reconciler.class); @@ -379,7 +374,7 @@ void updatesObservedGenerationOnNoUpdateUpdateControl() { } @Test - void updateObservedGenerationOnCustomResourceUpdate() { + void updateObservedGenerationOnCustomResourceUpdate() throws Exception { var observedGenResource = createObservedGenCustomResource(); Reconciler reconciler = mock(Reconciler.class); @@ -401,7 +396,7 @@ void updateObservedGenerationOnCustomResourceUpdate() { } @Test - void callErrorStatusHandlerIfImplemented() { + void callErrorStatusHandlerIfImplemented() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> { @@ -409,7 +404,7 @@ void callErrorStatusHandlerIfImplemented() { }; reconciler.errorHandler = (r, ri, e) -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); - return Optional.of(testCustomResource); + return ErrorStatusUpdateControl.updateStatus(testCustomResource); }; reconciliationDispatcher.handleExecution( @@ -433,7 +428,7 @@ public boolean isLastAttempt() { } @Test - void callErrorStatusHandlerEvenOnFirstError() { + void callErrorStatusHandlerEvenOnFirstError() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> { @@ -441,19 +436,62 @@ void callErrorStatusHandlerEvenOnFirstError() { }; reconciler.errorHandler = (r, ri, e) -> { testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); - return Optional.of(testCustomResource); + return ErrorStatusUpdateControl.updateStatus(testCustomResource); }; - reconciliationDispatcher.handleExecution( + var postExecControl = reconciliationDispatcher.handleExecution( + new ExecutionScope( + testCustomResource, null)); + verify(customResourceFacade, times(1)).updateStatus(testCustomResource); + verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), + any(), any()); + assertThat(postExecControl.exceptionDuringExecution()).isTrue(); + } + + @Test + void errorHandlerCanInstructNoRetryWithUpdate() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + reconciler.reconcile = (r, c) -> { + throw new IllegalStateException("Error Status Test"); + }; + reconciler.errorHandler = (r, ri, e) -> { + testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); + return ErrorStatusUpdateControl.updateStatus(testCustomResource).withNoRetry(); + }; + + var postExecControl = reconciliationDispatcher.handleExecution( new ExecutionScope( testCustomResource, null)); + + verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), + any(), any()); verify(customResourceFacade, times(1)).updateStatus(testCustomResource); + assertThat(postExecControl.exceptionDuringExecution()).isFalse(); + } + + @Test + void errorHandlerCanInstructNoRetryNoUpdate() { + testCustomResource.addFinalizer(DEFAULT_FINALIZER); + reconciler.reconcile = (r, c) -> { + throw new IllegalStateException("Error Status Test"); + }; + reconciler.errorHandler = (r, ri, e) -> { + testCustomResource.getStatus().setConfigMapStatus(ERROR_MESSAGE); + return ErrorStatusUpdateControl.noStatusUpdate().withNoRetry(); + }; + + var postExecControl = reconciliationDispatcher.handleExecution( + new ExecutionScope( + testCustomResource, null)); + verify(((ErrorStatusHandler) reconciler), times(1)).updateErrorStatus(eq(testCustomResource), any(), any()); + verify(customResourceFacade, times(0)).updateStatus(testCustomResource); + assertThat(postExecControl.exceptionDuringExecution()).isFalse(); } @Test - void schedulesReconciliationIfMaxDelayIsSet() { + void schedulesReconciliationIfMaxDelayIsSet() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -466,7 +504,7 @@ void schedulesReconciliationIfMaxDelayIsSet() { } @Test - void canSkipSchedulingMaxDelayIf() { + void canSkipSchedulingMaxDelayIf() throws Exception { testCustomResource.addFinalizer(DEFAULT_FINALIZER); reconciler.reconcile = (r, c) -> UpdateControl.noUpdate(); @@ -524,10 +562,11 @@ public DeleteControl cleanup(TestCustomResource resource, Context context) { } @Override - public Optional updateErrorStatus(TestCustomResource resource, - RetryInfo retryInfo, RuntimeException e) { - return errorHandler != null ? errorHandler.updateErrorStatus(resource, retryInfo, e) - : Optional.empty(); + public ErrorStatusUpdateControl updateErrorStatus( + TestCustomResource resource, + Context context, Exception e) { + return errorHandler != null ? errorHandler.updateErrorStatus(resource, context, e) + : ErrorStatusUpdateControl.noStatusUpdate(); } } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index c3b8632e02..17ea106104 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -1,6 +1,5 @@ package io.javaoperatorsdk.operator.sample.errorstatushandler; -import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; @@ -47,11 +46,13 @@ public int getNumberOfExecutions() { } @Override - public Optional updateErrorStatus( - ErrorStatusHandlerTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus( + ErrorStatusHandlerTestCustomResource resource, + Context context, Exception e) { log.info("Setting status."); ensureStatusExists(resource); - resource.getStatus().getMessages().add(ERROR_STATUS_MESSAGE + retryInfo.getAttemptCount()); - return Optional.of(resource); + resource.getStatus().getMessages() + .add(ERROR_STATUS_MESSAGE + context.getRetryInfo().orElseThrow().getAttemptCount()); + return ErrorStatusUpdateControl.updateStatus(resource); } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index aa8689e471..5e3d41b32f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -65,14 +65,15 @@ public KubernetesClient getKubernetesClient() { } @Override - public Optional updateErrorStatus( - StandaloneDependentTestCustomResource resource, RetryInfo retryInfo, RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus( + StandaloneDependentTestCustomResource resource, + Context context, Exception e) { // this can happen when a namespace is terminated in test if (e instanceof KubernetesClientException) { - return Optional.empty(); + return ErrorStatusUpdateControl.noStatusUpdate(); } errorOccurred = true; - return Optional.empty(); + return ErrorStatusUpdateControl.noStatusUpdate(); } public boolean isErrorOccurred() { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index eb66f918ab..b52d7931da 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -1,17 +1,10 @@ package io.javaoperatorsdk.operator.sample; -import java.util.Optional; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.Secret; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource; @@ -50,15 +43,16 @@ public UpdateControl reconcile(MySQLSchema schema, Context updateErrorStatus(MySQLSchema schema, RetryInfo retryInfo, - RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus(MySQLSchema schema, + Context context, + Exception e) { SchemaStatus status = new SchemaStatus(); status.setUrl(null); status.setUserName(null); status.setSecretName(null); status.setStatus("ERROR: " + e.getMessage()); schema.setStatus(status); - return Optional.empty(); + return ErrorStatusUpdateControl.updateStatus(schema); } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index ef0c2f5d17..bc7bb83894 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -210,9 +210,9 @@ private static String serviceName(WebPage nginx) { } @Override - public Optional updateErrorStatus( - WebPage resource, RetryInfo retryInfo, RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus( + WebPage resource, Context context, Exception e) { resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); + return ErrorStatusUpdateControl.updateStatus(resource); } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index 00a0d50d3b..ad3ef36840 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -75,10 +75,10 @@ private WebPageStatus createStatus(String configMapName) { } @Override - public Optional updateErrorStatus( - WebPage resource, RetryInfo retryInfo, RuntimeException e) { + public ErrorStatusUpdateControl updateErrorStatus( + WebPage resource, Context retryInfo, Exception e) { resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return Optional.of(resource); + return ErrorStatusUpdateControl.updateStatus(resource); } private void createDependentResources(KubernetesClient client) { @@ -91,7 +91,7 @@ private void createDependentResources(KubernetesClient client) { new CrudKubernetesDependentResource<>() { @Override - protected Deployment desired(WebPage webPage, Context context) { + protected Deployment desired(WebPage webPage, Context context) { var deploymentName = deploymentName(webPage); Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); deployment.getMetadata().setName(deploymentName); From 83393ad4beb1f03e5543505209f0e43806241324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20CROCQUESEL?= <88554524+scrocquesel@users.noreply.github.com> Date: Tue, 15 Mar 2022 21:01:29 +0100 Subject: [PATCH 0361/1608] refactor: fix Context is a raw type (#1037) * refactor: fix Context is a raw type * style: reformat --- .../operator/api/reconciler/ContextInitializer.java | 2 +- .../external/AbstractSimpleDependentResource.java | 2 +- .../kubernetes/KubernetesDependentResource.java | 2 +- .../processing/event/ReconciliationDispatcher.java | 2 +- .../javaoperatorsdk/operator/api/config/UtilsTest.java | 2 +- .../external/AbstractSimpleDependentResourceTest.java | 10 ++++++---- .../event/source/CustomResourceSelectorTest.java | 2 +- .../operator/sample/simple/DuplicateCRController.java | 2 +- .../operator/sample/simple/TestCustomReconciler.java | 4 ++-- .../sample/simple/TestCustomReconcilerOtherV1.java | 2 +- .../runtime/AnnotationControllerConfigurationTest.java | 4 ++-- .../runtime/DefaultConfigurationServiceTest.java | 6 +++--- .../CreateUpdateEventFilterTestReconciler.java | 3 ++- .../customfilter/CustomFilteringTestReconciler.java | 2 +- .../sample/deployment/DeploymentReconciler.java | 2 +- .../doubleupdate/DoubleUpdateTestCustomReconciler.java | 2 +- .../sample/event/EventSourceTestCustomReconciler.java | 2 +- .../InformerEventSourceTestCustomReconciler.java | 3 ++- .../sample/maxinterval/MaxIntervalTestReconciler.java | 2 +- .../MultiVersionCRDTestReconciler1.java | 3 ++- .../MultiVersionCRDTestReconciler2.java | 3 ++- .../ObservedGenerationTestReconciler.java | 3 ++- .../operator/sample/simple/TestReconciler.java | 4 ++-- .../StandaloneDependentTestReconciler.java | 3 ++- .../subresource/SubResourceTestCustomReconciler.java | 2 +- .../sample/dependent/SecretDependentResource.java | 4 ++-- .../operator/sample/DeploymentDependentResource.java | 2 +- .../operator/sample/ServiceDependentResource.java | 2 +- .../sample/WebPageReconcilerDependentResources.java | 7 ++++--- .../operator/sample/CustomServiceReconciler.java | 4 ++-- 30 files changed, 51 insertions(+), 42 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java index e076723d0c..c1213cce67 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ContextInitializer.java @@ -3,5 +3,5 @@ import io.fabric8.kubernetes.api.model.HasMetadata; public interface ContextInitializer

{ - void initContext(P primary, Context context); + void initContext(P primary, Context

context); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index 096e4265f6..850b290c12 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -70,7 +70,7 @@ protected R handleUpdate(R actual, R desired, P primary, Context

context) { return res; } - public Matcher.Result match(R actualResource, P primary, Context context) { + public Matcher.Result match(R actualResource, P primary, Context

context) { return matcher.match(actualResource, primary, context); } 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 a471df8c09..7ceadaf13c 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 @@ -99,7 +99,7 @@ public R update(R actual, R target, P primary, Context

context) { return prepare(target, primary, "Updating").replace(updatedActual); } - public Result match(R actualResource, P primary, Context context) { + public Result match(R actualResource, P primary, Context

context) { return matcher.match(actualResource, primary, context); } 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 180fbc2d28..6ca6959bbb 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 @@ -258,7 +258,7 @@ private void updatePostExecutionControlWithReschedule( } - private PostExecutionControl handleCleanup(R resource, Context context) { + private PostExecutionControl handleCleanup(R resource, Context context) { log.debug( "Executing delete for resource: {} with version: {}", getName(resource), 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 fbaa28ba23..8895922e82 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 @@ -79,7 +79,7 @@ public static class TestKubernetesDependentResource extends KubernetesDependentResource { @Override - protected Deployment desired(TestCustomResource primary, Context context) { + protected Deployment desired(TestCustomResource primary, Context context) { return null; } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java index 00557e7e61..b654ac5d1e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java @@ -113,7 +113,8 @@ public Optional fetchResource(HasMetadata primaryResourc @Override public SampleExternalResource create( - SampleExternalResource desired, TestCustomResource primary, Context context) { + SampleExternalResource desired, TestCustomResource primary, + Context context) { return SampleExternalResource.testResource1(); } @@ -122,15 +123,16 @@ public SampleExternalResource update( SampleExternalResource actual, SampleExternalResource desired, TestCustomResource primary, - Context context) { + Context context) { return SampleExternalResource.testResource1(); } @Override - public void delete(TestCustomResource primary, Context context) {} + public void delete(TestCustomResource primary, Context context) {} @Override - protected SampleExternalResource desired(TestCustomResource primary, Context context) { + protected SampleExternalResource desired(TestCustomResource primary, + Context context) { return SampleExternalResource.testResource1(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index e995d3b9c4..fd08a614af 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -151,7 +151,7 @@ public MyController(Consumer consumer) { @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { LOGGER.info("Received event on: {}", resource); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java index 86f0c36261..6b5b6ab08d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/DuplicateCRController.java @@ -10,7 +10,7 @@ public class DuplicateCRController implements Reconciler { @Override public UpdateControl reconcile(TestCustomResource resource, - Context context) { + Context context) { return UpdateControl.noUpdate(); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java index ec0e9bffbd..9595f3c13f 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java @@ -36,7 +36,7 @@ public TestCustomReconciler(KubernetesClient kubernetesClient, boolean updateSta @Override public DeleteControl cleanup( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { Boolean delete = kubernetesClient .configMaps() @@ -59,7 +59,7 @@ public DeleteControl cleanup( @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java index 9a45fbbf93..97f2fb9098 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconcilerOtherV1.java @@ -10,7 +10,7 @@ public class TestCustomReconcilerOtherV1 implements Reconciler reconcile(TestCustomResourceOtherV1 resource, - Context context) { + Context context) { return UpdateControl.noUpdate(); } } 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/runtime/AnnotationControllerConfigurationTest.java index b9556aba1c..fc44c3622e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java @@ -43,7 +43,7 @@ private static class OneDepReconciler implements Reconciler { public static final String CONFIGURED_NS = "foo"; @Override - public UpdateControl reconcile(ConfigMap resource, Context context) { + public UpdateControl reconcile(ConfigMap resource, Context context) { return null; } } @@ -51,7 +51,7 @@ public UpdateControl reconcile(ConfigMap resource, Context context) { private static class NoDepReconciler implements Reconciler { @Override - public UpdateControl reconcile(ConfigMap resource, Context context) { + public UpdateControl reconcile(ConfigMap resource, Context context) { return null; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index 6fd626b94b..d123aa4899 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -106,7 +106,7 @@ static class TestCustomFinalizerReconciler @Override public UpdateControl reconcile( - InnerCustomResource resource, Context context) { + InnerCustomResource resource, Context context) { return null; } @@ -123,7 +123,7 @@ static class NotAutomaticallyCreated implements Reconciler { @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { return null; } } @@ -133,7 +133,7 @@ static class TestCustomReconciler implements Reconciler { @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { return null; } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index 015f7a18bd..19e00e8ba2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -30,7 +30,8 @@ public class CreateUpdateEventFilterTestReconciler @Override public UpdateControl reconcile( - CreateUpdateEventFilterTestCustomResource resource, Context context) { + CreateUpdateEventFilterTestCustomResource resource, + Context context) { numberOfExecutions.incrementAndGet(); ConfigMap configMap = diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java index 2a13dc3092..1e42e8e6e1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFilteringTestReconciler.java @@ -14,7 +14,7 @@ public class CustomFilteringTestReconciler implements Reconciler reconcile(CustomFilteringTestResource resource, - Context context) { + Context context) { numberOfExecutions.incrementAndGet(); return UpdateControl.noUpdate(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java index 3992920884..62697d7e88 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/deployment/DeploymentReconciler.java @@ -26,7 +26,7 @@ public class DeploymentReconciler @Override public UpdateControl reconcile( - Deployment resource, Context context) { + Deployment resource, Context context) { log.info("Reconcile deployment: {}", resource); numberOfExecutions.incrementAndGet(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java index bd398d0a1c..11f0a54f3e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/doubleupdate/DoubleUpdateTestCustomReconciler.java @@ -24,7 +24,7 @@ public class DoubleUpdateTestCustomReconciler @Override public UpdateControl reconcile( - DoubleUpdateTestCustomResource resource, Context context) { + DoubleUpdateTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); log.info("Value: " + resource.getSpec().getValue()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java index 55eb61634f..cc4d083372 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/event/EventSourceTestCustomReconciler.java @@ -19,7 +19,7 @@ public class EventSourceTestCustomReconciler @Override public UpdateControl reconcile( - EventSourceTestCustomResource resource, Context context) { + EventSourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); ensureStatusExists(resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index d61aa7f0f2..97a99eed35 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -48,7 +48,8 @@ public List prepareEventSources( @Override public UpdateControl reconcile( - InformerEventSourceTestCustomResource resource, Context context) { + InformerEventSourceTestCustomResource resource, + Context context) { numberOfExecutions.incrementAndGet(); resource.setStatus(new InformerEventSourceTestCustomResourceStatus()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java index a5343c27a4..f9a34371ce 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java @@ -18,7 +18,7 @@ public class MaxIntervalTestReconciler @Override public UpdateControl reconcile( - MaxIntervalTestCustomResource resource, Context context) { + MaxIntervalTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); return UpdateControl.noUpdate(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java index ab8ad417be..de733ec6db 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java @@ -18,7 +18,8 @@ public class MultiVersionCRDTestReconciler1 @Override public UpdateControl reconcile( - MultiVersionCRDTestCustomResource1 resource, Context context) { + MultiVersionCRDTestCustomResource1 resource, + Context context) { log.info("Reconcile MultiVersionCRDTestCustomResource1: {}", resource.getMetadata().getName()); resource.getStatus().setValue1(resource.getStatus().getValue1() + 1); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java index d25297d1c6..138647029e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java @@ -20,7 +20,8 @@ public class MultiVersionCRDTestReconciler2 @Override public UpdateControl reconcile( - MultiVersionCRDTestCustomResource2 resource, Context context) { + MultiVersionCRDTestCustomResource2 resource, + Context context) { log.info("Reconcile MultiVersionCRDTestCustomResource2: {}", resource.getMetadata().getName()); resource.getStatus().setValue1(resource.getStatus().getValue1() + 1); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java index 65d7be4851..7a7964623f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java @@ -15,7 +15,8 @@ public class ObservedGenerationTestReconciler @Override public UpdateControl reconcile( - ObservedGenerationTestCustomResource resource, Context context) { + ObservedGenerationTestCustomResource resource, + Context context) { log.info("Reconcile ObservedGenerationTestCustomResource: {}", resource.getMetadata().getName()); return UpdateControl.updateStatus(resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index 94a7d36868..146c96c7db 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -59,7 +59,7 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { @Override public DeleteControl cleanup( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { Boolean delete = kubernetesClient .configMaps() @@ -82,7 +82,7 @@ public DeleteControl cleanup( @Override public UpdateControl reconcile( - TestCustomResource resource, Context context) { + TestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 5e3d41b32f..4a236e6e00 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -39,7 +39,8 @@ public List prepareEventSources( @Override public UpdateControl reconcile( - StandaloneDependentTestCustomResource primary, Context context) { + StandaloneDependentTestCustomResource primary, + Context context) { deploymentDependent.reconcile(primary, context); Optional deployment = deploymentDependent.getResource(primary); if (deployment.isEmpty()) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index 8bfe6dd67d..e520e94411 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -29,7 +29,7 @@ public class SubResourceTestCustomReconciler @Override public UpdateControl reconcile( - SubResourceTestCustomResource resource, Context context) { + SubResourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { throw new IllegalStateException("Finalizer is not present."); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 1b26eaf47a..15fff4d65e 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -27,7 +27,7 @@ private static String encode(String value) { } @Override - protected Secret desired(MySQLSchema schema, Context context) { + protected Secret desired(MySQLSchema schema, Context context) { final var password = RandomStringUtils .randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here final var name = schema.getMetadata().getName(); @@ -49,7 +49,7 @@ private String getSecretName(String name) { } @Override - public Result match(Secret actual, MySQLSchema primary, Context context) { + public Result match(Secret actual, MySQLSchema primary, Context context) { final var desiredSecretName = getSecretName(primary.getMetadata().getName()); return Result.nonComputed(actual.getMetadata().getName().equals(desiredSecretName)); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 56b80b6c82..b408a07ac9 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -19,7 +19,7 @@ private static String tomcatImage(Tomcat tomcat) { } @Override - protected Deployment desired(Tomcat tomcat, Context context) { + protected Deployment desired(Tomcat tomcat, Context context) { Deployment deployment = ReconcilerUtils.loadYaml(Deployment.class, getClass(), "deployment.yaml"); final ObjectMeta tomcatMetadata = tomcat.getMetadata(); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 66ddebf31e..3b618906ab 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -13,7 +13,7 @@ public class ServiceDependentResource extends KubernetesDependentResource, Updater { @Override - protected Service desired(Tomcat tomcat, Context context) { + protected Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); return new ServiceBuilder(ReconcilerUtils.loadYaml(Service.class, getClass(), "service.yaml")) .editMetadata() diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index ad3ef36840..d9efec3b19 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -51,7 +51,7 @@ public List prepareEventSources(EventSourceContext context } @Override - public UpdateControl reconcile(WebPage webPage, Context context) { + public UpdateControl reconcile(WebPage webPage, Context context) { if (webPage.getSpec().getHtml().contains("error")) { // special case just to showcase error if doing a demo throw new ErrorSimulationException("Simulating error"); @@ -166,7 +166,7 @@ private class ConfigMapDependentResource PrimaryToSecondaryMapper { @Override - protected ConfigMap desired(WebPage webPage, Context context) { + protected ConfigMap desired(WebPage webPage, Context context) { Map data = new HashMap<>(); data.put("index.html", webPage.getSpec().getHtml()); return new ConfigMapBuilder() @@ -180,7 +180,8 @@ protected ConfigMap desired(WebPage webPage, Context context) { } @Override - public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, Context context) { + public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, + Context context) { var res = super.update(actual, target, primary, context); var ns = actual.getMetadata().getNamespace(); log.info("Restarting pods because HTML has changed in {}", ns); diff --git a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java index 5e3348fadd..a899c50756 100644 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java +++ b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java @@ -30,14 +30,14 @@ public CustomServiceReconciler(KubernetesClient kubernetesClient) { } @Override - public DeleteControl cleanup(CustomService resource, Context context) { + public DeleteControl cleanup(CustomService resource, Context context) { log.info("Cleaning up for: {}", resource.getMetadata().getName()); return Reconciler.super.cleanup(resource, context); } @Override public UpdateControl reconcile( - CustomService resource, Context context) { + CustomService resource, Context context) { log.info("Reconciling: {}", resource.getMetadata().getName()); ServicePort servicePort = new ServicePort(); From 8a9cb345d5c6adcdd5dfe40e5306c5222b0a03c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 16 Mar 2022 09:28:44 +0100 Subject: [PATCH 0362/1608] doc: fix typo (#1041) --- docs/documentation/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 95924e24dd..60f3756440 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -196,7 +196,7 @@ custom resource, the reconciliation could be skipped. This is supported out of t default is not triggered if the change to the main custom resource does not increase the `.metadata.generation` field. Note that the increase of `.metada.generation` is handled automatically by Kubernetes. -To turn on this feature set `generationAwareEventProcessing` to `false` for the `Reconciler`. +To turn off this feature set `generationAwareEventProcessing` to `false` for the `Reconciler`. ## Support for Well Known (non-custom) Kubernetes Resources From 4b3178f6546494faff048b9dcef1e00244586fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 17 Mar 2022 09:42:30 +0100 Subject: [PATCH 0363/1608] fix: flaky test (#1044) --- .../operator/processing/event/EventProcessor.java | 4 ++++ .../processing/event/EventProcessorTest.java | 12 ++++++++++++ 2 files changed, 16 insertions(+) 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 4e7ffa4660..96d6a28fb2 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 @@ -364,4 +364,8 @@ public String toString() { return controllerName + " -> " + executionScope; } } + + public synchronized boolean isUnderProcessing(ResourceID resourceID) { + return underProcessing.contains(resourceID); + } } 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 0e968e13a7..0a124ea21d 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 @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.processing.event; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -24,6 +25,7 @@ import static io.javaoperatorsdk.operator.TestUtils.testCustomResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.*; @@ -152,16 +154,20 @@ void successfulExecutionResetsTheRetry() { eventProcessorWithRetry.handleEvent(event); verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(1)) .handleExecution(any()); + waitUntilProcessingFinished(eventProcessorWithRetry, event.getRelatedCustomResourceID()); eventProcessorWithRetry.handleEvent(event); verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(2)) .handleExecution(any()); + waitUntilProcessingFinished(eventProcessorWithRetry, event.getRelatedCustomResourceID()); eventProcessorWithRetry.handleEvent(event); verify(reconciliationDispatcherMock, timeout(SEPARATE_EXECUTION_TIMEOUT).times(3)) .handleExecution(executionScopeArgumentCaptor.capture()); + waitUntilProcessingFinished(eventProcessorWithRetry, event.getRelatedCustomResourceID()); log.info("Finished successfulExecutionResetsTheRetry"); + List executionScopes = executionScopeArgumentCaptor.getAllValues(); assertThat(executionScopes).hasSize(3); @@ -171,6 +177,12 @@ void successfulExecutionResetsTheRetry() { assertThat(executionScopes.get(1).getRetryInfo().isLastAttempt()).isEqualTo(false); } + private void waitUntilProcessingFinished(EventProcessor eventProcessor, + ResourceID relatedCustomResourceID) { + await().atMost(Duration.ofSeconds(3)) + .until(() -> !eventProcessor.isUnderProcessing(relatedCustomResourceID)); + } + @Test void scheduleTimedEventIfInstructedByPostExecutionControl() { var testDelay = 10000L; From c28078047846f2c0e315d8e107c597b63d4c6913 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Mar 2022 07:31:45 +0100 Subject: [PATCH 0364/1608] chore(deps): bump micrometer-core from 1.8.3 to 1.8.4 (#1045) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 965eb5206d..58c3e2fb5f 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 3.22.0 4.2.0 2.6.4 - 1.8.3 + 1.8.4 2.11 3.10.1 From f408a14b4e9a1285b5c2e6f4d7cd75cd57876660 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 18 Mar 2022 15:31:12 +0100 Subject: [PATCH 0365/1608] feat: improvements for Quarkus extension (#1043) * feat: extract DependentResource instantiation to DependentResourceFactory * feat: add AbstractPollingDependentResource * feat: add ResourceTypeAware interface * refactor: create DependentResources in Controller and remove DependentResourceManager --- .../api/config/ConfigurationService.java | 6 + .../dependent/AbstractDependentResource.java | 2 + .../dependent/DependentResourceFactory.java | 19 +++ .../dependent/ResourceTypeAware.java | 6 + .../operator/processing/Controller.java | 57 +++++++-- .../dependent/DependentResourceManager.java | 112 ------------------ .../AbstractCachingDependentResource.java | 15 ++- .../AbstractPollingDependentResource.java | 27 +++++ .../PerResourcePollingDependentResource.java | 27 +---- .../external/PollingDependentResource.java | 20 +--- .../CrudKubernetesDependentResource.java | 8 +- .../KubernetesDependentResource.java | 18 +-- .../operator/api/config/UtilsTest.java | 35 +++++- .../GenericKubernetesResourceMatcherTest.java | 2 +- .../sample/readonly/ReadOnlyDependent.java | 4 + .../StandaloneDependentTestReconciler.java | 13 +- .../dependent/SchemaDependentResource.java | 7 +- .../dependent/SecretDependentResource.java | 4 + .../sample/DeploymentDependentResource.java | 4 + .../sample/ServiceDependentResource.java | 4 + .../WebPageReconcilerDependentResources.java | 39 +++--- 21 files changed, 235 insertions(+), 194 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceTypeAware.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java 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 150af208bd..f13ae118f8 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 @@ -9,6 +9,7 @@ import io.fabric8.kubernetes.client.CustomResource; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -19,6 +20,7 @@ public interface ConfigurationService { ObjectMapper OBJECT_MAPPER = new ObjectMapper(); Cloner DEFAULT_CLONER = new Cloner() { + @SuppressWarnings("unchecked") @Override public HasMetadata clone(HasMetadata object) { try { @@ -126,4 +128,8 @@ default boolean closeClientOnStop() { default ObjectMapper getObjectMapper() { return OBJECT_MAPPER; } + + default DependentResourceFactory dependentResourceFactory() { + return new DependentResourceFactory() {}; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java index 0a750d836c..799355185c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java @@ -125,6 +125,7 @@ private RecentOperationCacheFiller eventSourceAsRecentOperationCacheFiller() return (RecentOperationCacheFiller) ((EventSourceProvider

) this).getEventSource(); } + @SuppressWarnings("unchecked") // this cannot be done in constructor since event source might be initialized later protected boolean isFilteringEventSource() { if (this instanceof EventSourceProvider) { @@ -135,6 +136,7 @@ protected boolean isFilteringEventSource() { } } + @SuppressWarnings("unchecked") // this cannot be done in constructor since event source might be initialized later protected boolean isRecentOperationCacheFiller() { if (this instanceof EventSourceProvider) { 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 new file mode 100644 index 0000000000..ffd0ba496f --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceFactory.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import java.lang.reflect.InvocationTargetException; + +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; + +public interface DependentResourceFactory { + + default > T createFrom(DependentResourceSpec spec) { + try { + return spec.getDependentResourceClass().getConstructor().newInstance(); + } catch (InstantiationException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException e) { + throw new IllegalArgumentException("Cannot instantiate DependentResource " + + spec.getDependentResourceClass().getCanonicalName(), e); + } + } + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceTypeAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceTypeAware.java new file mode 100644 index 0000000000..e83d39aebd --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceTypeAware.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +public interface ResourceTypeAware { + + Class resourceType(); +} 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 5fc1b9ede7..de71d54171 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 @@ -2,6 +2,7 @@ import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,9 +19,11 @@ import io.javaoperatorsdk.operator.OperatorException; 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.monitoring.Metrics; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; @@ -28,11 +31,13 @@ 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.processing.dependent.DependentResourceManager; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -@SuppressWarnings({"unchecked"}) +@SuppressWarnings({"unchecked", "rawtypes"}) @Ignore public class Controller

implements Reconciler

, LifecycleAware, EventSourceInitializer

{ @@ -43,7 +48,8 @@ public class Controller

implements Reconciler

, private final ControllerConfiguration

configuration; private final KubernetesClient kubernetesClient; private final EventSourceManager

eventSourceManager; - private final DependentResourceManager

dependents; + private final List dependents; + private final boolean contextInitializer; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -51,14 +57,42 @@ public Controller(Reconciler

reconciler, this.reconciler = reconciler; this.configuration = configuration; this.kubernetesClient = kubernetesClient; + contextInitializer = reconciler instanceof ContextInitializer; eventSourceManager = new EventSourceManager<>(this); - dependents = new DependentResourceManager<>(this); + + dependents = configuration.getDependentResources().stream() + .map(drs -> createAndConfigureFrom(drs, kubernetesClient)) + .collect(Collectors.toList()); + } + + @SuppressWarnings("rawtypes") + private DependentResource createAndConfigureFrom(DependentResourceSpec spec, + KubernetesClient client) { + final var dependentResource = + ConfigurationServiceProvider.instance().dependentResourceFactory().createFrom(spec); + + if (dependentResource instanceof KubernetesClientAware) { + ((KubernetesClientAware) dependentResource).setKubernetesClient(client); + } + + if (dependentResource instanceof DependentResourceConfigurator) { + final var configurator = (DependentResourceConfigurator) dependentResource; + spec.getDependentResourceConfiguration().ifPresent(configurator::configureWith); + } + return dependentResource; + } + + private void initContextIfNeeded(P resource, Context

context) { + if (contextInitializer) { + ((ContextInitializer

) reconciler).initContext(resource, context); + } } @Override public DeleteControl cleanup(P resource, Context

context) { - dependents.cleanup(resource, context); + initContextIfNeeded(resource, context); + dependents.forEach(dependent -> dependent.cleanup(resource, context)); try { return metrics().timeControllerExecution( @@ -90,7 +124,8 @@ public DeleteControl execute() { @Override public UpdateControl

reconcile(P resource, Context

context) throws Exception { - dependents.reconcile(resource, context); + initContextIfNeeded(resource, context); + dependents.forEach(dependent -> dependent.reconcile(resource, context)); return metrics().timeControllerExecution( new ControllerExecution<>() { @@ -131,8 +166,12 @@ private Metrics metrics() { @Override public List prepareEventSources(EventSourceContext

context) { - final var dependentSources = dependents.prepareEventSources(context); - List sources = new LinkedList<>(dependentSources); + List sources = new LinkedList<>(); + dependents.stream() + .filter(dependentResource -> dependentResource instanceof EventSourceProvider) + .map(EventSourceProvider.class::cast) + .map(provider -> provider.initEventSource(context)) + .forEach(sources::add); // add manually defined event sources if (reconciler instanceof EventSourceInitializer) { @@ -267,6 +306,6 @@ public void stop() { @SuppressWarnings("rawtypes") public List getDependents() { - return dependents.getDependents(); + return dependents; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java deleted file mode 100644 index 3dd3a2860e..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.javaoperatorsdk.operator.processing.dependent; - -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -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.ContextInitializer; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; -import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; -import io.javaoperatorsdk.operator.processing.Controller; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; - -@SuppressWarnings({"rawtypes", "unchecked"}) -@Ignore -public class DependentResourceManager

- implements EventSourceInitializer

, Reconciler

{ - - private static final Logger log = LoggerFactory.getLogger(DependentResourceManager.class); - - private final Reconciler

reconciler; - private final ControllerConfiguration

controllerConfiguration; - private List dependents; - - public DependentResourceManager(Controller

controller) { - this.reconciler = controller.getReconciler(); - this.controllerConfiguration = controller.getConfiguration(); - } - - @Override - public List prepareEventSources(EventSourceContext

context) { - final var dependentResources = controllerConfiguration.getDependentResources(); - final var sources = new ArrayList(dependentResources.size()); - dependents = - dependentResources.stream() - .map( - drc -> { - final var dependentResource = createAndConfigureFrom(drc, context.getClient()); - if (dependentResource instanceof EventSourceProvider) { - EventSourceProvider provider = (EventSourceProvider) dependentResource; - sources.add(provider.initEventSource(context)); - } - return dependentResource; - }) - .collect(Collectors.toList()); - return sources; - } - - @Override - public UpdateControl

reconcile(P resource, Context

context) { - initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.reconcile(resource, context)); - return UpdateControl.noUpdate(); - } - - @Override - public DeleteControl cleanup(P resource, Context

context) { - initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.cleanup(resource, context)); - return Reconciler.super.cleanup(resource, context); - } - - private void initContextIfNeeded(P resource, Context context) { - if (reconciler instanceof ContextInitializer) { - final var initializer = (ContextInitializer

) reconciler; - initializer.initContext(resource, context); - } - } - - private DependentResource createAndConfigureFrom(DependentResourceSpec dependentResourceSpec, - KubernetesClient client) { - try { - DependentResource dependentResource = - (DependentResource) dependentResourceSpec.getDependentResourceClass() - .getConstructor().newInstance(); - - if (dependentResource instanceof KubernetesClientAware) { - ((KubernetesClientAware) dependentResource).setKubernetesClient(client); - } - - if (dependentResource instanceof DependentResourceConfigurator) { - final var configurator = (DependentResourceConfigurator) dependentResource; - dependentResourceSpec.getDependentResourceConfiguration() - .ifPresent(configurator::configureWith); - } - return dependentResource; - } catch (InstantiationException | NoSuchMethodException | IllegalAccessException - | InvocationTargetException e) { - throw new IllegalStateException(e); - } - } - - public List getDependents() { - return dependents; - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java index 86f2c23d54..04b49bf0bf 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java @@ -3,16 +3,22 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceTypeAware; import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; public abstract class AbstractCachingDependentResource - extends AbstractDependentResource implements EventSourceProvider

{ + extends AbstractDependentResource + implements EventSourceProvider

, ResourceTypeAware { protected ExternalResourceCachingEventSource eventSource; + private final Class resourceType; + + protected AbstractCachingDependentResource(Class resourceType) { + this.resourceType = resourceType; + } public Optional fetchResource(P primaryResource) { return eventSource.getAssociated(primaryResource); @@ -23,8 +29,9 @@ public EventSource getEventSource() { return eventSource; } - protected Class resourceType() { - return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); + @Override + public Class resourceType() { + return resourceType; } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java new file mode 100644 index 0000000000..8c91dea15d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java @@ -0,0 +1,27 @@ +package io.javaoperatorsdk.operator.processing.dependent.external; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public abstract class AbstractPollingDependentResource + extends AbstractCachingDependentResource { + + public static final int DEFAULT_POLLING_PERIOD = 5000; + private long pollingPeriod; + + protected AbstractPollingDependentResource(Class resourceType) { + this(resourceType, DEFAULT_POLLING_PERIOD); + } + + public AbstractPollingDependentResource(Class resourceType, long pollingPeriod) { + super(resourceType); + this.pollingPeriod = pollingPeriod; + } + + public void setPollingPeriod(long pollingPeriod) { + this.pollingPeriod = pollingPeriod; + } + + public long getPollingPeriod() { + return pollingPeriod; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java index c5f9e2c888..11b18832a7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PerResourcePollingDependentResource.java @@ -5,36 +5,21 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource; -import static io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource.DEFAULT_POLLING_PERIOD; - public abstract class PerResourcePollingDependentResource - extends AbstractCachingDependentResource + extends AbstractPollingDependentResource implements PerResourcePollingEventSource.ResourceFetcher { - - protected long pollingPeriod; - - public PerResourcePollingDependentResource() { - this(DEFAULT_POLLING_PERIOD); + public PerResourcePollingDependentResource(Class resourceType) { + super(resourceType); } - public PerResourcePollingDependentResource(long pollingPeriod) { - this.pollingPeriod = pollingPeriod; + public PerResourcePollingDependentResource(Class resourceType, long pollingPeriod) { + super(resourceType, pollingPeriod); } @Override public EventSource initEventSource(EventSourceContext

context) { eventSource = new PerResourcePollingEventSource<>(this, context.getPrimaryCache(), - pollingPeriod, resourceType()); + getPollingPeriod(), resourceType()); return eventSource; } - - public PerResourcePollingDependentResource setPollingPeriod(long pollingPeriod) { - this.pollingPeriod = pollingPeriod; - return this; - } - - public long getPollingPeriod() { - return pollingPeriod; - } - } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java index 18e0c66b60..58723a6f11 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/PollingDependentResource.java @@ -10,27 +10,19 @@ import io.javaoperatorsdk.operator.processing.event.source.polling.PollingEventSource; public abstract class PollingDependentResource - extends AbstractCachingDependentResource implements Supplier> { + extends AbstractPollingDependentResource implements Supplier> { - public static final int DEFAULT_POLLING_PERIOD = 5000; - protected long pollingPeriod; - - public PollingDependentResource() { - this(DEFAULT_POLLING_PERIOD); + public PollingDependentResource(Class resourceType) { + super(resourceType); } - public PollingDependentResource(long pollingPeriod) { - this.pollingPeriod = pollingPeriod; + public PollingDependentResource(Class resourceType, long pollingPeriod) { + super(resourceType, pollingPeriod); } @Override public EventSource initEventSource(EventSourceContext

context) { - eventSource = new PollingEventSource<>(this, pollingPeriod, resourceType()); + eventSource = new PollingEventSource<>(this, getPollingPeriod(), resourceType()); return eventSource; } - - public PollingDependentResource setPollingPeriod(long pollingPeriod) { - this.pollingPeriod = pollingPeriod; - return this; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java index 88e9bdc7ae..b363e81b51 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java @@ -12,6 +12,10 @@ * @param

Primary Resource */ public abstract class CrudKubernetesDependentResource - extends - KubernetesDependentResource implements Creator, Updater, Deleter

{ + extends KubernetesDependentResource + implements Creator, Updater, Deleter

{ + + public CrudKubernetesDependentResource(Class resourceType) { + super(resourceType); + } } 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 7ceadaf13c..9439d74865 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 @@ -11,7 +11,6 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.api.config.Utils; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; @@ -21,6 +20,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceTypeAware; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -31,7 +31,7 @@ public abstract class KubernetesDependentResource extends AbstractDependentResource - implements KubernetesClientAware, EventSourceProvider

, + implements KubernetesClientAware, EventSourceProvider

, ResourceTypeAware, DependentResourceConfigurator { private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class); @@ -41,15 +41,17 @@ public abstract class KubernetesDependentResource matcher; private final ResourceUpdatePreProcessor processor; + private final Class resourceType; @SuppressWarnings("unchecked") - public KubernetesDependentResource() { + public KubernetesDependentResource(Class resourceType) { + this.resourceType = resourceType; matcher = this instanceof Matcher ? (Matcher) this - : GenericKubernetesResourceMatcher.matcherFor(resourceType(), this); + : GenericKubernetesResourceMatcher.matcherFor(resourceType, this); processor = this instanceof ResourceUpdatePreProcessor ? (ResourceUpdatePreProcessor) this - : GenericResourceUpdatePreProcessor.processorFor(resourceType()); + : GenericResourceUpdatePreProcessor.processorFor(resourceType); } @Override @@ -140,9 +142,9 @@ public KubernetesDependentResource setInformerEventSource( return this; } - @SuppressWarnings("unchecked") - protected Class resourceType() { - return (Class) Utils.getFirstTypeArgumentFromExtendedClass(getClass()); + @Override + public Class resourceType() { + return resourceType; } @Override 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 8895922e82..0a24d4f26d 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 @@ -1,14 +1,21 @@ package io.javaoperatorsdk.operator.api.config; +import java.util.Optional; + import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; 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.*; +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 UtilsTest { @@ -75,12 +82,32 @@ void getsFirstTypeArgumentFromExtendedClass() { assertThat(res).isEqualTo(Deployment.class); } - public static class TestKubernetesDependentResource - extends KubernetesDependentResource { + @Test + void getsFirstTypeArgumentFromInterface() { + assertThat(Utils.getFirstTypeArgumentFromInterface(TestDependentResource.class)) + .isEqualTo(Deployment.class); + } + + public static class TestDependentResource + implements DependentResource { @Override - protected Deployment desired(TestCustomResource primary, Context context) { + public ReconcileResult reconcile(TestCustomResource primary, + Context context) { return null; } + + @Override + public Optional getResource(TestCustomResource primaryResource) { + return Optional.empty(); + } + } + + public static class TestKubernetesDependentResource + extends KubernetesDependentResource { + + public TestKubernetesDependentResource() { + super(Deployment.class); + } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java index 1eb7ea1bb4..bad10d551d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcherTest.java @@ -32,7 +32,7 @@ void checksIfDesiredValuesAreTheSame() { var actual = createDeployment(); final var desired = createDeployment(); final var matcher = GenericKubernetesResourceMatcher.matcherFor(Deployment.class, - new KubernetesDependentResource<>() { + new KubernetesDependentResource<>(Deployment.class) { @Override protected Deployment desired(HasMetadata primary, Context context) { final var currentCase = Optional.ofNullable(primary) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java index 0285d47b83..15fb02a0b6 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/readonly/ReadOnlyDependent.java @@ -4,4 +4,8 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; public class ReadOnlyDependent extends KubernetesDependentResource { + + public ReadOnlyDependent() { + super(ConfigMap.class); + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 4a236e6e00..e740032d6e 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -7,7 +7,14 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.javaoperatorsdk.operator.ReconcilerUtils; -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.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +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.Creator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; @@ -86,6 +93,10 @@ private static class DeploymentDependentResource extends implements Creator, Updater { + public DeploymentDependentResource() { + super(Deployment.class); + } + @Override protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { 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 f5a5e9ea18..273cd086a1 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 @@ -16,7 +16,8 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; -import io.javaoperatorsdk.operator.sample.*; +import io.javaoperatorsdk.operator.sample.MySQLDbConfig; +import io.javaoperatorsdk.operator.sample.MySQLSchema; import io.javaoperatorsdk.operator.sample.schema.Schema; import io.javaoperatorsdk.operator.sample.schema.SchemaService; @@ -35,6 +36,10 @@ public class SchemaDependentResource private MySQLDbConfig dbConfig; + public SchemaDependentResource() { + super(Schema.class); + } + @Override public void configureWith(ResourcePollerConfig config) { this.dbConfig = config.getMySQLDbConfig(); diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 15fff4d65e..60c4c4d2b0 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -22,6 +22,10 @@ public class SecretDependentResource extends KubernetesDependentResource implements Creator, Updater { + public DeploymentDependentResource() { + super(Deployment.class); + } + private static String tomcatImage(Tomcat tomcat) { return "tomcat:" + tomcat.getSpec().getVersion(); } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index 3b618906ab..c52553851d 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -12,6 +12,10 @@ public class ServiceDependentResource extends KubernetesDependentResource implements Creator, Updater { + public ServiceDependentResource() { + super(Service.class); + } + @Override protected Service desired(Tomcat tomcat, Context context) { final ObjectMeta tomcatMetadata = tomcat.getMetadata(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index d9efec3b19..90f747d158 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -1,15 +1,27 @@ package io.javaoperatorsdk.operator.sample; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; -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.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +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.processing.dependent.kubernetes.CrudKubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; @@ -88,7 +100,7 @@ private void createDependentResources(KubernetesClient client) { .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); this.deploymentDR = - new CrudKubernetesDependentResource<>() { + new CrudKubernetesDependentResource<>(Deployment.class) { @Override protected Deployment desired(WebPage webPage, Context context) { @@ -114,18 +126,13 @@ protected Deployment desired(WebPage webPage, Context context) { new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); return deployment; } - - @Override - protected Class resourceType() { - return Deployment.class; - } }; deploymentDR.setKubernetesClient(client); deploymentDR.configureWith(new KubernetesDependentResourceConfig() .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); this.serviceDR = - new CrudKubernetesDependentResource<>() { + new CrudKubernetesDependentResource<>(Service.class) { @Override protected Service desired(WebPage webPage, Context context) { @@ -137,11 +144,6 @@ protected Service desired(WebPage webPage, Context context) { service.getSpec().setSelector(labels); return service; } - - @Override - protected Class resourceType() { - return Service.class; - } }; serviceDR.setKubernetesClient(client); serviceDR.configureWith(new KubernetesDependentResourceConfig() @@ -162,8 +164,11 @@ public static String serviceName(WebPage webPage) { private class ConfigMapDependentResource extends CrudKubernetesDependentResource - implements - PrimaryToSecondaryMapper { + implements PrimaryToSecondaryMapper { + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } @Override protected ConfigMap desired(WebPage webPage, Context context) { From 392c36562bbc866472c199d5cf68fd4689a3518c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 18 Mar 2022 15:46:04 +0100 Subject: [PATCH 0366/1608] fix: remove deprecated api (#842) --- .../processing/event/ReconciliationDispatcher.java | 2 +- .../operator/SubResourceUpdateIT.java | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) 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 6ca6959bbb..235a27481d 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 @@ -335,7 +335,7 @@ public R updateStatus(R resource) { return resourceOperation .inNamespace(resource.getMetadata().getNamespace()) .withName(getName(resource)) - .updateStatus(resource); + .replaceStatus(resource); } public R replaceWithLock(R resource) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index 85e11c7287..bd335c9ae7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -70,15 +70,13 @@ void ifNoFinalizerPresentFirstAddsTheFinalizerThenExecutesControllerAgain() { } /** - * Not that here status sub-resource update will fail on optimistic locking. This solves a tricky - * situation: If this would not happen (no optimistic locking on status sub-resource) we could - * receive and store an event while processing the controller method. But this event would always - * fail since its resource version is outdated already. + * The update status actually does optimistic locking in the background but fabric8 client retries + * it with an up-to-date resource version. */ @Test void updateCustomResourceAfterSubResourceChange() { SubResourceTestCustomResource resource = createTestCustomResource("1"); - operator.create(SubResourceTestCustomResource.class, resource); + resource = operator.create(SubResourceTestCustomResource.class, resource); // waits for the resource to start processing waitXms(EVENT_RECEIVE_WAIT); @@ -89,9 +87,9 @@ void updateCustomResourceAfterSubResourceChange() { // wait for sure, there are no more events waitXms(WAIT_AFTER_EXECUTION); - // there is no event on status update processed - assertThat(TestUtils.getNumberOfExecutions(operator)) - .isEqualTo(3); + // note that both is valid, since after the update of the status the event receive lags, + // that will result in a third execution + assertThat(TestUtils.getNumberOfExecutions(operator)).isBetween(2, 3); } void awaitStatusUpdated(String name) { From 5a9bbe287952b032e84745329b46b11c8eb3ae1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 18 Mar 2022 20:32:23 +0100 Subject: [PATCH 0367/1608] feat: separate interface for cleanup part of reconciler (#1035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Chris Laprun Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> --- docs/documentation/features.md | 14 ++-- .../operator/ReconcilerUtils.java | 2 +- .../AnnotationControllerConfiguration.java | 2 +- .../api/config/ControllerConfiguration.java | 7 +- .../ControllerConfigurationOverrider.java | 2 +- .../DefaultControllerConfiguration.java | 2 +- .../operator/api/reconciler/Cleaner.java | 30 +++++++ .../operator/api/reconciler/Constants.java | 1 - .../operator/api/reconciler/Context.java | 2 +- .../reconciler/ControllerConfiguration.java | 7 +- .../api/reconciler/DefaultContext.java | 2 +- .../operator/api/reconciler/Reconciler.java | 25 ------ .../api/reconciler/dependent/Deleter.java | 6 ++ .../dependent/DependentResource.java | 2 - .../reconciler/dependent/ReconcileResult.java | 4 +- .../DependentResourceConfigurator.java | 2 +- .../{ => managed}/KubernetesClientAware.java | 2 +- .../ManagedDependentResourceContext.java | 3 +- .../operator/processing/Controller.java | 84 ++++++++++++------- .../dependent/AbstractDependentResource.java | 17 +--- .../dependent/Creator.java | 2 +- .../dependent/DesiredEqualsMatcher.java | 2 +- .../dependent/Matcher.java | 2 +- .../dependent/Updater.java | 4 +- .../AbstractCachingDependentResource.java | 2 +- .../AbstractSimpleDependentResource.java | 12 +-- ...a => CRUDKubernetesDependentResource.java} | 14 ++-- .../CRUKubernetesDependentResource.java | 21 +++++ .../GenericKubernetesResourceMatcher.java | 2 +- .../GenericResourceUpdatePreProcessor.java | 1 - .../KubernetesDependentResource.java | 11 ++- .../ResourceUpdatePreProcessor.java | 2 +- .../event/ReconciliationDispatcher.java | 33 +++----- .../ControllerResourceEventSource.java | 2 +- .../controller/ResourceEventFilter.java | 26 +++--- .../controller/ResourceEventFilters.java | 10 +-- .../operator/ReconcilerUtilsTest.java | 6 -- .../operator/processing/ControllerTest.java | 30 ++++--- .../AbstractSimpleDependentResourceTest.java | 13 +-- ...GenericResourceUpdatePreProcessorTest.java | 1 - .../event/ReconciliationDispatcherTest.java | 15 ++-- .../source/CustomResourceSelectorTest.java | 2 +- .../ControllerResourceEventSourceTest.java | 13 ++- .../sample/simple/TestCustomReconciler.java | 3 +- .../operator/CleanerForReconcilerIT.java | 49 +++++++++++ .../operator/ControllerExecutionIT.java | 17 ++++ ...terForManagedDependentResourcesOnlyIT.java | 54 ++++++++++++ .../operator/EventSourceIT.java | 1 - .../io/javaoperatorsdk/operator/RetryIT.java | 1 - .../operator/SubResourceUpdateIT.java | 2 - .../operator/UpdatingResAndSubResIT.java | 3 - .../DefaultConfigurationServiceTest.java | 4 +- .../CleanerForReconcilerCustomResource.java | 17 ++++ .../CleanerForReconcilerTestReconciler.java | 39 +++++++++ ...anerForManagedDependentCustomResource.java | 17 ++++ ...anerForManagedDependentTestReconciler.java | 28 +++++++ .../ConfigMapDependentResource.java | 44 ++++++++++ ...CreateUpdateEventFilterTestReconciler.java | 4 +- .../sample/customfilter/CustomFlagFilter.java | 4 +- .../customfilter/CustomFlagFilter2.java | 4 +- .../ErrorStatusHandlerTestReconciler.java | 4 +- ...formerEventSourceTestCustomReconciler.java | 4 +- .../MaxIntervalTestReconciler.java | 7 +- .../MultiVersionCRDTestReconciler1.java | 4 +- .../MultiVersionCRDTestReconciler2.java | 6 +- .../ObservedGenerationTestReconciler.java | 9 +- .../retry/RetryTestCustomReconciler.java | 6 -- .../sample/simple/TestReconciler.java | 17 ++-- .../StandaloneDependentTestReconciler.java | 17 +--- .../SubResourceTestCustomReconciler.java | 6 -- .../ReconcilerImplemented2Interfaces.java | 10 +-- .../sample/MySQLSchemaReconciler.java | 4 +- .../dependent/SchemaDependentResource.java | 13 +-- .../dependent/SecretDependentResource.java | 4 +- .../sample/MySQLSchemaOperatorE2E.java | 38 +++++---- .../sample/DeploymentDependentResource.java | 4 +- .../sample/ServiceDependentResource.java | 4 +- .../operator/sample/TomcatReconciler.java | 3 - .../operator/sample/WebappReconciler.java | 15 +--- .../operator/sample/WebPageReconciler.java | 7 +- .../WebPageReconcilerDependentResources.java | 12 +-- .../sample/CustomServiceReconciler.java | 25 +++--- 82 files changed, 604 insertions(+), 348 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/{ => managed}/DependentResourceConfigurator.java (55%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/{ => managed}/KubernetesClientAware.java (69%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/{ => managed}/ManagedDependentResourceContext.java (96%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/AbstractDependentResource.java (92%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/Creator.java (79%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/DesiredEqualsMatcher.java (91%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/Matcher.java (92%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler => processing}/dependent/Updater.java (68%) rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/{CrudKubernetesDependentResource.java => CRUDKubernetesDependentResource.java} (50%) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUKubernetesDependentResource.java rename operator-framework-core/src/main/java/io/javaoperatorsdk/operator/{api/reconciler/dependent => processing/dependent/kubernetes}/ResourceUpdatePreProcessor.java (78%) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerTestReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentTestReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java diff --git a/docs/documentation/features.md b/docs/documentation/features.md index 3f2a8843c3..7b6143ca02 100644 --- a/docs/documentation/features.md +++ b/docs/documentation/features.md @@ -37,9 +37,13 @@ execution. [Kubernetes finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) make sure that a reconciliation happens when a custom resource is instructed to be deleted. Typical case when it's useful, when an operator is down (pod not running). Without a finalizer the reconciliation - thus the cleanup - -i.e. [`Reconciler.cleanup(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b91221bb54af19761a617bf18eef381e8ceb3b4c/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java#L31) +i.e. [`Cleaner.cleanup(...)`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b82c1f106968cb3eb18835c5e9cd1e4d5c40362e/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java#L28-L28) would not happen if a custom resource is deleted. +To use finalizers the reconciler have to implement [`Cleaner

`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b82c1f106968cb3eb18835c5e9cd1e4d5c40362e/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java) interface. +In other words, finalizer is added if the `Reconciler` implements `Cleaner` interface. If not, no +finalizer is added and/or removed. + Finalizers are automatically added by the framework as the first step, thus after a custom resource is created, but before the first reconciliation. The finalizer is added via a separate Kubernetes API call. As a result of this update, the finalizer will be present. The subsequent event will be received, which will trigger the first reconciliation. @@ -50,7 +54,6 @@ in some specific corner cases, when there would be a long waiting period for som The name of the finalizers can be specified, in case it is not, a name will be generated. -Automatic finalizer handling can be turned off, so when configured no finalizer will be added or removed. See [`@ControllerConfiguration`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java) annotation for more details. @@ -66,7 +69,7 @@ When automatic finalizer handling is turned off, the `Reconciler.cleanup(...)` m case when a delete event received. So it does not make sense to implement this method and turn off finalizer at the same time. -## The `reconcile` and `cleanup` Methods of [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) +## The `reconcile` and [`cleanup`](https://github.com/java-operator-sdk/java-operator-sdk/blob/b82c1f106968cb3eb18835c5e9cd1e4d5c40362e/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java) Methods on a [`Reconciler`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java) The lifecycle of a custom resource can be clearly separated into two phases from the perspective of an operator. When a custom resource is created or update, or on the other hand when the custom resource is deleted - or rather marked for @@ -75,9 +78,8 @@ deletion in case a finalizer is used. This separation-related logic is automatically handled by the framework. The framework will always call `reconcile` method, unless the custom resource is [marked from deletion](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/#how-finalizers-work) -. From the point when the custom resource is marked from deletion, only the `cleanup` method is called. - -If there is **no finalizer** in place (see Finalizer Support section), the `cleanup` method is **not called**. +. From the point when the custom resource is marked from deletion, only the `cleanup` method is called, of course +only if the reconciler implements the `Cleaner` interface. ### Using [`UpdateControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/UpdateControl.java) and [`DeleteControl`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DeleteControl.java) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java index 98a43492f9..a99abe4d8d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java @@ -42,7 +42,7 @@ public void setApiVersion(String s) { throw new UnsupportedOperationException(); } }; - return Constants.NO_FINALIZER.equals(finalizer) || validator.isFinalizerValid(finalizer); + return validator.isFinalizerValid(finalizer); } public static String getResourceTypeNameWithVersion(Class resourceClass) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index 72d34e16a8..eb57d2c71a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -39,7 +39,7 @@ public String getName() { } @Override - public String getFinalizer() { + public String getFinalizerName() { if (annotation == null || annotation.finalizerName().isBlank()) { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } else { 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 03485412b6..aff1d04e8b 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 @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; -import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -18,7 +17,7 @@ default String getName() { return ReconcilerUtils.getDefaultReconcilerName(getAssociatedReconcilerClassName()); } - default String getFinalizer() { + default String getFinalizerName() { return ReconcilerUtils.getDefaultFinalizerName(getResourceClass()); } @@ -32,10 +31,6 @@ default RetryConfiguration getRetryConfiguration() { return RetryConfiguration.DEFAULT; } - default boolean useFinalizer() { - return !Constants.NO_FINALIZER.equals(getFinalizer()); - } - /** * Allow controllers to filter events before they are passed to the * {@link io.javaoperatorsdk.operator.processing.event.EventHandler}. 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 87297b8d49..da0b6d0a52 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 @@ -25,7 +25,7 @@ public class ControllerConfigurationOverrider { private final List> dependentResourceSpecs; private ControllerConfigurationOverrider(ControllerConfiguration original) { - finalizer = original.getFinalizer(); + finalizer = original.getFinalizerName(); generationAware = original.isGenerationAware(); namespaces = new HashSet<>(original.getNamespaces()); retry = original.getRetryConfiguration(); 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 index 4545c9885e..890254d830 100644 --- 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 @@ -66,7 +66,7 @@ public String getResourceTypeName() { } @Override - public String getFinalizer() { + public String getFinalizerName() { return finalizer; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java new file mode 100644 index 0000000000..e2e70a246d --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java @@ -0,0 +1,30 @@ +package io.javaoperatorsdk.operator.api.reconciler; + +import io.fabric8.kubernetes.api.model.HasMetadata; + +public interface Cleaner

{ + + /** + * Note that this method turns on automatic finalizer usage. + * + * The implementation should delete the associated component(s). This method is called when an + * object is marked for deletion. After it's executed the custom resource finalizer is + * automatically removed by the framework; unless the return value is + * {@link DeleteControl#noFinalizerRemoval()}, which indicates that the controller has determined + * that the resource should not be deleted yet. This is usually a corner case, when a cleanup is + * tried again eventually. + * + *

+ * It's important for implementations of this method to be idempotent, since it can be called + * several times. + * + * @param resource the resource that is marked for deletion + * @param context the context with which the operation is executed + * @return {@link DeleteControl#defaultDelete()} - so the finalizer is automatically removed after + * the call. {@link DeleteControl#noFinalizerRemoval()} if you don't want to remove the + * finalizer to indicate that the resource should not be deleted after all, in which case + * the controller should restore the resource's state appropriately. + */ + DeleteControl cleanup(P resource, Context

context); + +} 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 85b3a00807..e08e168ca4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -4,7 +4,6 @@ public final class Constants { public static final String EMPTY_STRING = ""; public static final String WATCH_CURRENT_NAMESPACE = "JOSDK_WATCH_CURRENT"; - public static final String NO_FINALIZER = "JOSDK_NO_FINALIZER"; public static final long NO_RECONCILIATION_MAX_INTERVAL = -1L; private Constants() {} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 2c3fb6ff1e..b2c2270f0b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext; public interface Context

{ 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 0feaa34b2d..fe69738b1a 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 @@ -15,9 +15,10 @@ String name() default Constants.EMPTY_STRING; /** - * Optional finalizer name, if it is not provided, one will be automatically generated. If the - * provided value is the value specified by {@link Constants#NO_FINALIZER}, then no finalizer will - * be added to custom resources. + * Optional finalizer name, if it is not provided, one will be automatically generated. Note that + * finalizers are only added when Reconciler implement {@link Cleaner} interface, or at least one + * managed dependent resource implement + * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter} interface. * * @return the finalizer name */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index d80be8b3ae..0a7b36339d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -4,7 +4,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext; import io.javaoperatorsdk.operator.processing.Controller; public class DefaultContext

implements Context

{ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java index 446df36ca1..771b169fd1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Reconciler.java @@ -15,29 +15,4 @@ public interface Reconciler { */ UpdateControl reconcile(R resource, Context context) throws Exception; - /** - * Note that this method is used in combination with finalizers. If automatic finalizer handling - * is turned off for the controller, this method is not called. - * - * The implementation should delete the associated component(s). This method is called when an - * object is marked for deletion. After it's executed the custom resource finalizer is - * automatically removed by the framework; unless the return value is - * {@link DeleteControl#noFinalizerRemoval()}, which indicates that the controller has determined - * that the resource should not be deleted yet. This is usually a corner case, when a cleanup is - * tried again eventually. - * - *

- * It's important for implementations of this method to be idempotent, since it can be called - * several times. - * - * @param resource the resource that is marked for deletion - * @param context the context with which the operation is executed - * @return {@link DeleteControl#defaultDelete()} - so the finalizer is automatically removed after - * the call. {@link DeleteControl#noFinalizerRemoval()} if you don't want to remove the - * finalizer to indicate that the resource should not be deleted after all, in which case - * the controller should restore the resource's state appropriately. - */ - default DeleteControl cleanup(R resource, Context context) { - return DeleteControl.defaultDelete(); - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java index 0a1bfc1bab..f6bd2682ca 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Deleter.java @@ -3,6 +3,12 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +/** + * DependentResource can implement this interface to denote it requires explicit logic to clean up + * resources. + * + * @param

primary resource type + */ @FunctionalInterface public interface Deleter

{ void delete(P primary, Context

context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java index 377e4aea22..1dcfbe57d5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java @@ -8,7 +8,5 @@ public interface DependentResource { ReconcileResult reconcile(P primary, Context

context); - default void cleanup(P primary, Context

context) {} - Optional getResource(P primaryResource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java index 392b213e81..86a931c480 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java @@ -4,8 +4,8 @@ public class ReconcileResult { - private R resource; - private Operation operation; + private final R resource; + private final Operation operation; public static ReconcileResult resourceCreated(T resource) { return new ReconcileResult<>(resource, Operation.CREATED); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java similarity index 55% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java index b51de9f35f..bbb4f75da9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResourceConfigurator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DependentResourceConfigurator.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; public interface DependentResourceConfigurator { void configureWith(C config); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java similarity index 69% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java index b8ef888ec3..0e787753a5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/KubernetesClientAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; import io.fabric8.kubernetes.client.KubernetesClient; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java similarity index 96% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java index 9543b7d582..745f1e3151 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ManagedDependentResourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; import java.util.Collections; import java.util.List; @@ -7,6 +7,7 @@ import java.util.stream.Collectors; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; /** * Contextual information related to {@link DependentResource} either to retrieve the actual 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 de71d54171..7ee63d760f 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 @@ -22,6 +22,7 @@ import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; @@ -30,16 +31,17 @@ import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @SuppressWarnings({"unchecked", "rawtypes"}) @Ignore -public class Controller

implements Reconciler

, +public class Controller

implements Reconciler

, Cleaner

, LifecycleAware, EventSourceInitializer

{ private static final Logger log = LoggerFactory.getLogger(Controller.class); @@ -50,6 +52,8 @@ public class Controller

implements Reconciler

, private final EventSourceManager

eventSourceManager; private final List dependents; private final boolean contextInitializer; + private final boolean hasDeleterDependents; + private final boolean isCleaner; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -61,9 +65,19 @@ public Controller(Reconciler

reconciler, eventSourceManager = new EventSourceManager<>(this); + final var hasDeleterHolder = new boolean[] {false}; dependents = configuration.getDependentResources().stream() .map(drs -> createAndConfigureFrom(drs, kubernetesClient)) + .peek(d -> { + // check if any dependent implements Deleter to record that fact + if (!hasDeleterHolder[0] && d instanceof Deleter) { + hasDeleterHolder[0] = true; + } + }) .collect(Collectors.toList()); + + hasDeleterDependents = hasDeleterHolder[0]; + isCleaner = reconciler instanceof Cleaner; } @SuppressWarnings("rawtypes") @@ -92,31 +106,40 @@ private void initContextIfNeeded(P resource, Context

context) { @Override public DeleteControl cleanup(P resource, Context

context) { initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.cleanup(resource, context)); - try { - return metrics().timeControllerExecution( - new ControllerExecution<>() { - @Override - public String name() { - return "cleanup"; - } - - @Override - public String controllerName() { - return configuration.getName(); - } - - @Override - public String successTypeName(DeleteControl deleteControl) { - return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; - } - - @Override - public DeleteControl execute() { - return reconciler.cleanup(resource, context); - } - }); + return metrics() + .timeControllerExecution( + new ControllerExecution<>() { + @Override + public String name() { + return "cleanup"; + } + + @Override + public String controllerName() { + return configuration.getName(); + } + + @Override + public String successTypeName(DeleteControl deleteControl) { + return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; + } + + @Override + public DeleteControl execute() { + if (hasDeleterDependents) { + dependents.stream() + .filter(d -> d instanceof Deleter) + .map(Deleter.class::cast) + .forEach(deleter -> deleter.delete(resource, context)); + } + if (isCleaner) { + return ((Cleaner

) reconciler).cleanup(resource, context); + } else { + return DeleteControl.defaultDelete(); + } + } + }); } catch (Exception e) { throw new OperatorException(e); } @@ -125,8 +148,6 @@ public DeleteControl execute() { @Override public UpdateControl

reconcile(P resource, Context

context) throws Exception { initContextIfNeeded(resource, context); - dependents.forEach(dependent -> dependent.reconcile(resource, context)); - return metrics().timeControllerExecution( new ControllerExecution<>() { @Override @@ -153,6 +174,7 @@ public String successTypeName(UpdateControl

result) { @Override public UpdateControl

execute() throws Exception { + dependents.forEach(dependent -> dependent.reconcile(resource, context)); return reconciler.reconcile(resource, context); } }); @@ -304,6 +326,10 @@ public void stop() { } } + public boolean useFinalizer() { + return isCleaner || hasDeleterDependents; + } + @SuppressWarnings("rawtypes") public List getDependents() { return dependents; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java similarity index 92% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java index 799355185c..4758ba5eab 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java @@ -1,10 +1,11 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; public abstract class AbstractDependentResource @@ -13,16 +14,13 @@ 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; - protected Deleter

deleter; @SuppressWarnings("unchecked") public AbstractDependentResource() { creator = creatable ? (Creator) this : null; updater = updatable ? (Updater) this : null; - deleter = deletable ? (Deleter

) this : null; } @Override @@ -147,13 +145,6 @@ protected boolean isRecentOperationCacheFiller() { } } - @Override - public void cleanup(P primary, Context

context) { - if (isDeletable(primary, context)) { - deleter.delete(primary, context); - } - } - protected R desired(P primary, Context

context) { throw new IllegalStateException( "desired method must be implemented if this DependentResource can be created and/or updated"); @@ -169,8 +160,4 @@ protected boolean isUpdatable(P primary, Context

context) { return updatable; } - @SuppressWarnings("unused") - protected boolean isDeletable(P primary, Context

context) { - return deletable; - } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Creator.java similarity index 79% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Creator.java index 48af8fb61b..b4143906bd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Creator.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Creator.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java similarity index 91% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java index 2cf8b155ff..459d7951d6 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DesiredEqualsMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java similarity index 92% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java index 09e78911e6..84290fe464 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Matcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import java.util.Optional; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java similarity index 68% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java index d49ccc632e..828f9ad785 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Updater.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Updater.java @@ -1,8 +1,8 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; +import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; public interface Updater { R update(R actual, R desired, P primary, Context

context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java index 04b49bf0bf..c45e50db9a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractCachingDependentResource.java @@ -3,9 +3,9 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceTypeAware; +import io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource; import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java index 850b290c12..2b91e6d75f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java @@ -4,10 +4,10 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DesiredEqualsMatcher; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.DesiredEqualsMatcher; +import io.javaoperatorsdk.operator.processing.dependent.Matcher; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.ConcurrentHashMapCache; import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; @@ -51,11 +51,13 @@ public ReconcileResult reconcile(P primary, Context

context) { return super.reconcile(primary, context); } - public void cleanup(P primary, Context

context) { - super.cleanup(primary, context); + public final void delete(P primary, Context

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

context); + @Override protected R handleCreate(R desired, P primary, Context

context) { var res = this.creator.create(desired, primary, context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java similarity index 50% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java index b363e81b51..05195e39b9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CrudKubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDKubernetesDependentResource.java @@ -1,21 +1,21 @@ package io.javaoperatorsdk.operator.processing.dependent.kubernetes; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; /** - * Adaptor Class for standalone mode for resources that manages Create, Update and Delete + * Adaptor Class for standalone mode for resources that manages Create, Read, Update and Delete * * @param Managed resource * @param

Primary Resource */ -public abstract class CrudKubernetesDependentResource - extends KubernetesDependentResource - implements Creator, Updater, Deleter

{ +public abstract class CRUDKubernetesDependentResource + extends + KubernetesDependentResource implements Creator, Updater, Deleter

{ - public CrudKubernetesDependentResource(Class resourceType) { + public CRUDKubernetesDependentResource(Class resourceType) { super(resourceType); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUKubernetesDependentResource.java new file mode 100644 index 0000000000..7670367936 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUKubernetesDependentResource.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; + +/** + * Adaptor Class for standalone mode for resources that manages Create, Read and Update + * + * @param Managed resource + * @param

Primary Resource + */ +public abstract class CRUKubernetesDependentResource + extends + KubernetesDependentResource implements Creator, Updater { + + + public CRUKubernetesDependentResource(Class resourceType) { + super(resourceType); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java index 0bbe19837e..f2c789e267 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java @@ -7,7 +7,7 @@ import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; +import io.javaoperatorsdk.operator.processing.dependent.Matcher; public class GenericKubernetesResourceMatcher implements Matcher { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java index 0e0624d183..e35913af68 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessor.java @@ -6,7 +6,6 @@ import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; public abstract class GenericResourceUpdatePreProcessor implements ResourceUpdatePreProcessor { 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 9439d74865..233deb020d 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 @@ -14,14 +14,13 @@ import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; -import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; -import io.javaoperatorsdk.operator.api.reconciler.dependent.KubernetesClientAware; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceTypeAware; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource; +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.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java similarity index 78% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java index 55ebcca230..3b88884e6a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ResourceUpdatePreProcessor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/ResourceUpdatePreProcessor.java @@ -1,4 +1,4 @@ -package io.javaoperatorsdk.operator.api.reconciler.dependent; +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Context; 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 235a27481d..2a0f0997f0 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 @@ -16,7 +16,6 @@ import io.javaoperatorsdk.operator.api.reconciler.DefaultContext; import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; @@ -60,9 +59,9 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) log.debug("Handling dispatch for resource {}", getName(resource)); final var markedForDeletion = resource.isMarkedForDeletion(); - if (markedForDeletion && shouldNotDispatchToDelete(resource)) { + if (markedForDeletion && shouldNotDispatchToCleanup(resource)) { log.debug( - "Skipping delete of resource {} because finalizer(s) {} don't allow processing yet", + "Skipping cleanup of resource {} because finalizer(s) {} don't allow processing yet", getName(resource), resource.getMetadata().getFinalizers()); return PostExecutionControl.defaultDispatch(); @@ -76,25 +75,17 @@ private PostExecutionControl handleDispatch(ExecutionScope executionScope) } } - /** - * Determines whether the given resource should be dispatched to the controller's - * {@link Reconciler#cleanup(HasMetadata, Context)} method - * - * @param resource the resource to be potentially deleted - * @return {@code true} if the resource should be handed to the controller's - * {@link Reconciler#cleanup(HasMetadata, Context)} method, {@code false} otherwise - */ - private boolean shouldNotDispatchToDelete(R resource) { - // we don't dispatch to delete if the controller is configured to use a finalizer but that + private boolean shouldNotDispatchToCleanup(R resource) { + // we don't dispatch to cleanup if the controller is configured to use a finalizer but that // finalizer is not present (which means it's already been removed) - return !configuration().useFinalizer() || (configuration().useFinalizer() - && !resource.hasFinalizer(configuration().getFinalizer())); + return !controller.useFinalizer() || (controller.useFinalizer() + && !resource.hasFinalizer(configuration().getFinalizerName())); } private PostExecutionControl handleReconcile( ExecutionScope executionScope, R originalResource, Context context) throws Exception { - if (configuration().useFinalizer() - && !originalResource.hasFinalizer(configuration().getFinalizer())) { + if (controller.useFinalizer() + && !originalResource.hasFinalizer(configuration().getFinalizerName())) { /* * We always add the finalizer if missing and the controller is configured to use a finalizer. * We execute the controller processing only for processing the event sent as a results of the @@ -265,12 +256,12 @@ private PostExecutionControl handleCleanup(R resource, Context context) { getVersion(resource)); DeleteControl deleteControl = controller.cleanup(resource, context); - final var useFinalizer = configuration().useFinalizer(); + final var useFinalizer = controller.useFinalizer(); if (useFinalizer) { // note that we don't reschedule here even if instructed. Removing finalizer means that // cleanup is finished, nothing left to done if (deleteControl.isRemoveFinalizer() - && resource.hasFinalizer(configuration().getFinalizer())) { + && resource.hasFinalizer(configuration().getFinalizerName())) { R customResource = removeFinalizer(resource); return PostExecutionControl.customResourceUpdated(customResource); } @@ -289,7 +280,7 @@ private PostExecutionControl handleCleanup(R resource, Context context) { private void updateCustomResourceWithFinalizer(R resource) { log.debug( "Adding finalizer for resource: {} version: {}", getUID(resource), getVersion(resource)); - resource.addFinalizer(configuration().getFinalizer()); + resource.addFinalizer(configuration().getFinalizerName()); replace(resource); } @@ -304,7 +295,7 @@ private R removeFinalizer(R resource) { "Removing finalizer on resource: {} with version: {}", getUID(resource), getVersion(resource)); - resource.removeFinalizer(configuration().getFinalizer()); + resource.removeFinalizer(configuration().getFinalizerName()); return customResourceFacade.replaceWithLock(resource); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java index b43666660c..fe9d802630 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSource.java @@ -59,7 +59,7 @@ public void eventReceived(ResourceAction action, T resource, T oldResource) { log.debug("Event received for resource: {}", getName(resource)); MDCUtils.addResourceInfo(resource); controller.getEventSourceManager().broadcastOnResourceEvent(action, resource, oldResource); - if (filter.acceptChange(controller.getConfiguration(), oldResource, resource)) { + if (filter.acceptChange(controller, oldResource, resource)) { getEventHandler().handleEvent( new ResourceEvent(action, ResourceID.fromResource(resource))); } else { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java index 497c9016b7..17cb27fd71 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilter.java @@ -1,29 +1,29 @@ package io.javaoperatorsdk.operator.processing.event.source.controller; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.Controller; /** * A functional interface to determine whether resource events should be processed by the SDK. This * allows users to more finely tuned which events trigger a reconciliation than was previously * possible (where the logic was limited to generation-based checking). * - * @param the type of custom resources handled by this filter + * @param

the type of custom resources handled by this filter */ @FunctionalInterface -public interface ResourceEventFilter { +public interface ResourceEventFilter

{ /** * Determines whether the change between the old version of the resource and the new one needs to * be propagated to the controller or not. * - * @param configuration the target controller's configuration + * @param controller the target controller * @param oldResource the old version of the resource, null if no old resource available * @param newResource the new version of the resource * @return {@code true} if the change needs to be propagated to the controller, {@code false} * otherwise */ - boolean acceptChange(ControllerConfiguration configuration, T oldResource, T newResource); + boolean acceptChange(Controller

controller, P oldResource, P newResource); /** * Combines this filter with the provided one with an AND logic, i.e. the resulting filter will @@ -32,11 +32,11 @@ public interface ResourceEventFilter { * @param other the possibly {@code null} other filter to combine this one with * @return a composite filter implementing the AND logic between this and the provided filter */ - default ResourceEventFilter and(ResourceEventFilter other) { + default ResourceEventFilter

and(ResourceEventFilter

other) { return other == null ? this - : (ControllerConfiguration configuration, T oldResource, T newResource) -> { - boolean result = acceptChange(configuration, oldResource, newResource); - return result && other.acceptChange(configuration, oldResource, newResource); + : (Controller

controller, P oldResource, P newResource) -> { + boolean result = acceptChange(controller, oldResource, newResource); + return result && other.acceptChange(controller, oldResource, newResource); }; } @@ -48,11 +48,11 @@ default ResourceEventFilter and(ResourceEventFilter other) { * @param other the possibly {@code null} other filter to combine this one with * @return a composite filter implementing the OR logic between this and the provided filter */ - default ResourceEventFilter or(ResourceEventFilter other) { + default ResourceEventFilter

or(ResourceEventFilter

other) { return other == null ? this - : (ControllerConfiguration configuration, T oldResource, T newResource) -> { - boolean result = acceptChange(configuration, oldResource, newResource); - return result || other.acceptChange(configuration, oldResource, newResource); + : (Controller

controller, P oldResource, P newResource) -> { + boolean result = acceptChange(controller, oldResource, newResource); + return result || other.acceptChange(controller, oldResource, newResource); }; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java index c314af7f2e..5a22cafac8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ResourceEventFilters.java @@ -8,9 +8,9 @@ public final class ResourceEventFilters { private static final ResourceEventFilter USE_FINALIZER = - (configuration, oldResource, newResource) -> { - if (configuration.useFinalizer()) { - final var finalizer = configuration.getFinalizer(); + (controller, oldResource, newResource) -> { + if (controller.useFinalizer()) { + final var finalizer = controller.getConfiguration().getFinalizerName(); boolean oldFinalizer = oldResource == null || oldResource.hasFinalizer(finalizer); boolean newFinalizer = newResource.hasFinalizer(finalizer); @@ -21,8 +21,8 @@ public final class ResourceEventFilters { }; private static final ResourceEventFilter GENERATION_AWARE = - (configuration, oldResource, newResource) -> { - final var generationAware = configuration.isGenerationAware(); + (controller, oldResource, newResource) -> { + final var generationAware = controller.getConfiguration().isGenerationAware(); return oldResource == null || !generationAware || oldResource.getMetadata().getGeneration() < newResource.getMetadata().getGeneration(); }; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index e137a2ff11..5081767980 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -7,7 +7,6 @@ import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; -import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.sample.simple.TestCustomReconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -40,11 +39,6 @@ void defaultFinalizerShouldWork() { assertTrue(isFinalizerValid(getDefaultFinalizerName(TestCustomResource.class))); } - @Test - void noFinalizerMarkerShouldWork() { - assertTrue(isFinalizerValid(Constants.NO_FINALIZER)); - } - @Test void equalsSpecObject() { var d1 = createTestDeployment(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index ba31d6634e..2f5b9bf4ba 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -7,23 +7,24 @@ import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @SuppressWarnings("unchecked") class ControllerTest { + + final ControllerConfiguration configuration = mock(ControllerConfiguration.class); + final Reconciler reconciler = mock(Reconciler.class); + @Test void crdShouldNotBeCheckedForNativeResources() { final var client = MockKubernetesClient.client(Secret.class); - final var reconciler = mock(Reconciler.class); - final var configuration = mock(ControllerConfiguration.class); + when(configuration.getResourceClass()).thenReturn(Secret.class); final var controller = new Controller(reconciler, configuration, client); @@ -34,8 +35,6 @@ void crdShouldNotBeCheckedForNativeResources() { @Test void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { final var client = MockKubernetesClient.client(TestCustomResource.class); - final var reconciler = mock(Reconciler.class); - final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); try { @@ -52,8 +51,6 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { void crdShouldBeCheckedForCustomResourcesByDefault() { ConfigurationServiceProvider.reset(); final var client = MockKubernetesClient.client(TestCustomResource.class); - final var reconciler = mock(Reconciler.class); - final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); final var controller = new Controller(reconciler, configuration, client); @@ -62,4 +59,15 @@ void crdShouldBeCheckedForCustomResourcesByDefault() { assertThrows(MissingCRDException.class, controller::start); verify(client, times(1)).apiextensions(); } + + @Test + void usesFinalizerIfThereIfReconcilerImplementsCleaner() { + Reconciler reconciler = mock(Reconciler.class, withSettings().extraInterfaces(Cleaner.class)); + when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); + + final var controller = new Controller(reconciler, + configuration, MockKubernetesClient.client(TestCustomResource.class)); + + assertThat(controller.useFinalizer()).isTrue(); + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java index b654ac5d1e..0b9517eb81 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResourceTest.java @@ -9,9 +9,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.TestUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.SampleExternalResource; import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache; @@ -84,7 +84,7 @@ void updatePutsNewResourceToCache() { @Test void deleteRemovesResourceFromCache() { - simpleDependentResource.cleanup(TestUtils.testCustomResource1(), null); + simpleDependentResource.delete(TestUtils.testCustomResource1(), null); verify(updatableCacheMock, times(1)).remove(any()); } @@ -111,6 +111,10 @@ public Optional fetchResource(HasMetadata primaryResourc return Optional.ofNullable(supplier.get()); } + @Override + protected void deleteResource(TestCustomResource primary, + Context context) {} + @Override public SampleExternalResource create( SampleExternalResource desired, TestCustomResource primary, @@ -127,9 +131,6 @@ public SampleExternalResource update( return SampleExternalResource.testResource1(); } - @Override - public void delete(TestCustomResource primary, Context context) {} - @Override protected SampleExternalResource desired(TestCustomResource primary, Context context) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java index 802be9c3d9..e67d579adc 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericResourceUpdatePreProcessorTest.java @@ -9,7 +9,6 @@ import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; 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 2e6ee4aa0d..ccf218d3d0 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 @@ -93,9 +93,8 @@ private ReconciliationDispatcher init(R customResourc CustomResourceFacade customResourceFacade, boolean useFinalizer) { configuration = configuration == null ? mock(ControllerConfiguration.class) : configuration; - final var finalizer = useFinalizer ? DEFAULT_FINALIZER : Constants.NO_FINALIZER; - when(configuration.getFinalizer()).thenReturn(finalizer); - when(configuration.useFinalizer()).thenCallRealMethod(); + + when(configuration.getFinalizerName()).thenReturn(DEFAULT_FINALIZER); when(configuration.getName()).thenReturn("EventDispatcherTestController"); when(configuration.getResourceClass()).thenReturn((Class) customResource.getClass()); when(configuration.getRetryConfiguration()).thenReturn(RetryConfiguration.DEFAULT); @@ -103,7 +102,12 @@ private ReconciliationDispatcher init(R customResourc .thenReturn(Optional.of(Duration.ofHours(RECONCILIATION_MAX_INTERVAL))); Controller controller = new Controller<>(reconciler, configuration, - MockKubernetesClient.client(customResource.getClass())); + MockKubernetesClient.client(customResource.getClass())) { + @Override + public boolean useFinalizer() { + return useFinalizer; + } + }; controller.start(); return new ReconciliationDispatcher<>(controller, customResourceFacade); @@ -539,7 +543,8 @@ public ExecutionScope executionScopeWithCREvent(T res } private class TestReconciler - implements Reconciler, ErrorStatusHandler { + implements Reconciler, Cleaner, + ErrorStatusHandler { private BiFunction> reconcile; private BiFunction cleanup; private ErrorStatusHandler errorHandler; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java index fd08a614af..2d943deb80 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/CustomResourceSelectorTest.java @@ -134,7 +134,7 @@ public TestCustomResource newMyResource(String app, String namespace) { public static class MyConfiguration extends DefaultControllerConfiguration { public MyConfiguration() { - super(MyController.class.getCanonicalName(), "mycontroller", null, Constants.NO_FINALIZER, + super(MyController.class.getCanonicalName(), "mycontroller", null, Constants.EMPTY_STRING, false, null, null, null, null, TestCustomResource.class, null, null); } 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 d06cf97fe8..1c467e0490 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 @@ -34,7 +34,7 @@ public void setup() { } @Test - public void skipsEventHandlingIfGenerationNotIncreased() { + void skipsEventHandlingIfGenerationNotIncreased() { TestCustomResource customResource = TestUtils.testCustomResource(); customResource.getMetadata().setFinalizers(List.of(FINALIZER)); customResource.getMetadata().setGeneration(2L); @@ -50,7 +50,7 @@ public void skipsEventHandlingIfGenerationNotIncreased() { } @Test - public void dontSkipEventHandlingIfMarkedForDeletion() { + void dontSkipEventHandlingIfMarkedForDeletion() { TestCustomResource customResource1 = TestUtils.testCustomResource(); source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); @@ -63,7 +63,7 @@ public void dontSkipEventHandlingIfMarkedForDeletion() { } @Test - public void normalExecutionIfGenerationChanges() { + void normalExecutionIfGenerationChanges() { TestCustomResource customResource1 = TestUtils.testCustomResource(); source.eventReceived(ResourceAction.UPDATED, customResource1, customResource1); @@ -75,7 +75,7 @@ public void normalExecutionIfGenerationChanges() { } @Test - public void handlesAllEventIfNotGenerationAware() { + void handlesAllEventIfNotGenerationAware() { source = new ControllerResourceEventSource<>(new TestController(false)); setup(); @@ -124,6 +124,11 @@ public TestController(boolean generationAware) { public EventSourceManager getEventSourceManager() { return eventSourceManager; } + + @Override + public boolean useFinalizer() { + return true; + } } private static class TestConfiguration extends diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java index 9595f3c13f..3413b620b3 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestCustomReconciler.java @@ -15,7 +15,8 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; @ControllerConfiguration(generationAwareEventProcessing = false) -public class TestCustomReconciler implements Reconciler { +public class TestCustomReconciler + implements Reconciler, Cleaner { private static final Logger log = LoggerFactory.getLogger(TestCustomReconciler.class); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java new file mode 100644 index 0000000000..5356204491 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/CleanerForReconcilerIT.java @@ -0,0 +1,49 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.cleanerforreconciler.CleanerForReconcilerCustomResource; +import io.javaoperatorsdk.operator.sample.cleanerforreconciler.CleanerForReconcilerTestReconciler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class CleanerForReconcilerIT { + + public static final String TEST_RESOURCE_NAME = "cleaner-for-reconciler-test1"; + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder().withReconciler(new CleanerForReconcilerTestReconciler()).build(); + + + @Test + void addsFinalizerAndCallsCleanupIfCleanerImplemented() { + var testResource = createTestResource(); + operator.create(CleanerForReconcilerCustomResource.class, testResource); + + await().until(() -> !operator.get(CleanerForReconcilerCustomResource.class, TEST_RESOURCE_NAME) + .getMetadata().getFinalizers().isEmpty()); + + operator.delete(CleanerForReconcilerCustomResource.class, testResource); + + await().until( + () -> operator.get(CleanerForReconcilerCustomResource.class, TEST_RESOURCE_NAME) == null); + + CleanerForReconcilerTestReconciler reconciler = + (CleanerForReconcilerTestReconciler) operator.getFirstReconciler(); + assertThat(reconciler.getNumberOfExecutions()).isEqualTo(1); + assertThat(reconciler.getNumberOfCleanupExecutions()).isEqualTo(1); + } + + private CleanerForReconcilerCustomResource createTestResource() { + CleanerForReconcilerCustomResource cr = new CleanerForReconcilerCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName(TEST_RESOURCE_NAME); + return cr; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java index 0a3ed114a1..3688b55d3c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ControllerExecutionIT.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -42,6 +43,22 @@ void eventIsSkippedChangedOnMetadataOnlyUpdate() { assertThat(TestUtils.getNumberOfExecutions(operator)).isEqualTo(1); } + @Test + void cleanupExecuted() { + operator.getControllerOfType(TestReconciler.class).setUpdateStatus(true); + + TestCustomResource resource = TestUtils.testCustomResource(); + resource = operator.create(TestCustomResource.class, resource); + + awaitResourcesCreatedOrUpdated(); + awaitStatusUpdated(); + operator.delete(TestCustomResource.class, resource); + + await().atMost(Duration.ofSeconds(1)) + .until(() -> ((TestReconciler) operator.getFirstReconciler()) + .getNumberOfCleanupExecutions() == 1); + } + void awaitResourcesCreatedOrUpdated() { await("config map created") .atMost(5, TimeUnit.SECONDS) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java new file mode 100644 index 0000000000..94c7c7798d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/DeleterForManagedDependentResourcesOnlyIT.java @@ -0,0 +1,54 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.junit.OperatorExtension; +import io.javaoperatorsdk.operator.sample.cleanermanageddependent.CleanerForManagedDependentCustomResource; +import io.javaoperatorsdk.operator.sample.cleanermanageddependent.CleanerForManagedDependentTestReconciler; +import io.javaoperatorsdk.operator.sample.cleanermanageddependent.ConfigMapDependentResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class DeleterForManagedDependentResourcesOnlyIT { + + public static final String TEST_RESOURCE_NAME = "cleaner-for-reconciler-test1"; + + @RegisterExtension + OperatorExtension operator = + OperatorExtension.builder().withReconciler(new CleanerForManagedDependentTestReconciler()) + .build(); + + + @Test + void addsFinalizerAndCallsCleanupIfCleanerImplemented() { + var testResource = createTestResource(); + operator.create(CleanerForManagedDependentCustomResource.class, testResource); + + await().until( + () -> !operator.get(CleanerForManagedDependentCustomResource.class, TEST_RESOURCE_NAME) + .getMetadata().getFinalizers().isEmpty()); + + operator.delete(CleanerForManagedDependentCustomResource.class, testResource); + + await().until( + () -> operator.get(CleanerForManagedDependentCustomResource.class, + TEST_RESOURCE_NAME) == null); + + CleanerForManagedDependentTestReconciler reconciler = + (CleanerForManagedDependentTestReconciler) operator.getFirstReconciler(); + + assertThat(reconciler.getNumberOfExecutions()).isEqualTo(1); + assertThat(ConfigMapDependentResource.getNumberOfCleanupExecutions()).isEqualTo(1); + } + + private CleanerForManagedDependentCustomResource createTestResource() { + CleanerForManagedDependentCustomResource cr = new CleanerForManagedDependentCustomResource(); + cr.setMetadata(new ObjectMeta()); + cr.getMetadata().setName(TEST_RESOURCE_NAME); + return cr; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index eea996cb3b..5aaeb0bab4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -41,7 +41,6 @@ public EventSourceTestCustomResource createTestCustomResource(String id) { new ObjectMetaBuilder() .withName("eventsource-" + id) .withNamespace(operator.getNamespace()) - .withFinalizers(EventSourceTestCustomReconciler.FINALIZER_NAME) .build()); resource.setSpec(new EventSourceTestCustomResourceSpec()); resource.getSpec().setValue(id); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java index 02b7b7af9e..7a951aadc7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/RetryIT.java @@ -65,7 +65,6 @@ public static RetryTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("retrysource-" + id) - .withFinalizers(RetryTestCustomReconciler.FINALIZER_NAME) .build()); resource.setKind("retrysample"); resource.setSpec(new RetryTestCustomResourceSpec()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java index bd335c9ae7..61bc9f927c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/SubResourceUpdateIT.java @@ -99,7 +99,6 @@ void awaitStatusUpdated(String name) { () -> { SubResourceTestCustomResource cr = operator.get(SubResourceTestCustomResource.class, name); - assertThat(cr.getMetadata().getFinalizers()).hasSize(1); assertThat(cr).isNotNull(); assertThat(cr.getStatus()).isNotNull(); assertThat(cr.getStatus().getState()) @@ -112,7 +111,6 @@ public SubResourceTestCustomResource createTestCustomResource(String id) { resource.setMetadata( new ObjectMetaBuilder() .withName("subresource-" + id) - .withFinalizers(SubResourceTestCustomReconciler.FINALIZER_NAME) .build()); resource.setKind("SubresourceSample"); resource.setSpec(new SubResourceTestCustomResourceSpec()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java index d86ad51d60..df885110d4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/UpdatingResAndSubResIT.java @@ -54,11 +54,8 @@ void awaitStatusUpdated(String name) { () -> { DoubleUpdateTestCustomResource cr = operator.get(DoubleUpdateTestCustomResource.class, name); - assertThat(cr) .isNotNull(); - assertThat(cr.getMetadata().getFinalizers()) - .hasSize(1); assertThat(cr.getStatus()) .isNotNull(); assertThat(cr.getStatus().getState()) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java index d123aa4899..c5bf845853 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/DefaultConfigurationServiceTest.java @@ -79,7 +79,7 @@ void returnsValuesFromControllerAnnotationFinalizer() { configuration.getResourceTypeName()); assertEquals( ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class), - configuration.getFinalizer()); + configuration.getFinalizerName()); assertEquals(TestCustomResource.class, configuration.getResourceClass()); assertFalse(configuration.isGenerationAware()); } @@ -88,7 +88,7 @@ void returnsValuesFromControllerAnnotationFinalizer() { void returnCustomerFinalizerNameIfSet() { final var reconciler = new TestCustomFinalizerReconciler(); final var configuration = configurationService.getConfigurationFor(reconciler); - assertEquals(CUSTOM_FINALIZER_NAME, configuration.getFinalizer()); + assertEquals(CUSTOM_FINALIZER_NAME, configuration.getFinalizerName()); } @Test diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerCustomResource.java new file mode 100644 index 0000000000..7a4b255d28 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.cleanerforreconciler; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("CleanerForReconcilerCustomResource") +@ShortNames("cfr") +public class CleanerForReconcilerCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerTestReconciler.java new file mode 100644 index 0000000000..fad1bb7798 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanerforreconciler/CleanerForReconcilerTestReconciler.java @@ -0,0 +1,39 @@ +package io.javaoperatorsdk.operator.sample.cleanerforreconciler; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration +public class CleanerForReconcilerTestReconciler + implements Reconciler, + Cleaner, + TestExecutionInfoProvider { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + CleanerForReconcilerCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + public int getNumberOfCleanupExecutions() { + return numberOfCleanupExecutions.get(); + } + + @Override + public DeleteControl cleanup(CleanerForReconcilerCustomResource resource, + Context context) { + numberOfCleanupExecutions.addAndGet(1); + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentCustomResource.java new file mode 100644 index 0000000000..9e18e657aa --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.cleanermanageddependent; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("CleanerForReconcilerCustomResource") +@ShortNames("cfr") +public class CleanerForManagedDependentCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentTestReconciler.java new file mode 100644 index 0000000000..6be29c5092 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/CleanerForManagedDependentTestReconciler.java @@ -0,0 +1,28 @@ +package io.javaoperatorsdk.operator.sample.cleanermanageddependent; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration(dependents = {@Dependent(type = ConfigMapDependentResource.class)}) +public class CleanerForManagedDependentTestReconciler + implements Reconciler, + TestExecutionInfoProvider { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + CleanerForManagedDependentCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java new file mode 100644 index 0000000000..332e2e7534 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java @@ -0,0 +1,44 @@ +package io.javaoperatorsdk.operator.sample.cleanermanageddependent; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; + +public class ConfigMapDependentResource extends + CRUDKubernetesDependentResource { + + private static final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(CleanerForManagedDependentCustomResource primary, + Context context) { + + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMeta()); + configMap.getMetadata().setName(primary.getMetadata().getName()); + configMap.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + HashMap data = new HashMap<>(); + data.put("key1", "val1"); + configMap.setData(data); + return configMap; + } + + @Override + public void delete(CleanerForManagedDependentCustomResource primary, + Context context) { + super.delete(primary, context); + numberOfCleanupExecutions.incrementAndGet(); + } + + public static int getNumberOfCleanupExecutions() { + return numberOfCleanupExecutions.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java index 19e00e8ba2..4c1ffc4312 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/createupdateeventfilter/CreateUpdateEventFilterTestReconciler.java @@ -15,9 +15,7 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class CreateUpdateEventFilterTestReconciler implements Reconciler, EventSourceInitializer, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java index 58d2ed1ede..bba45a44ac 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter.java @@ -1,12 +1,12 @@ package io.javaoperatorsdk.operator.sample.customfilter; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class CustomFlagFilter implements ResourceEventFilter { @Override - public boolean acceptChange(ControllerConfiguration configuration, + public boolean acceptChange(Controller configuration, CustomFilteringTestResource oldResource, CustomFilteringTestResource newResource) { return newResource.getSpec().isFilter1(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java index f38a8c9553..ae6b5d684f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/customfilter/CustomFlagFilter2.java @@ -1,12 +1,12 @@ package io.javaoperatorsdk.operator.sample.customfilter; -import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; +import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; public class CustomFlagFilter2 implements ResourceEventFilter { @Override - public boolean acceptChange(ControllerConfiguration configuration, + public boolean acceptChange(Controller configuration, CustomFilteringTestResource oldResource, CustomFilteringTestResource newResource) { return newResource.getSpec().isFilter2(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java index 17ea106104..412a784b7c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/errorstatushandler/ErrorStatusHandlerTestReconciler.java @@ -8,9 +8,7 @@ import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class ErrorStatusHandlerTestReconciler implements Reconciler, TestExecutionInfoProvider, ErrorStatusHandler { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java index 97a99eed35..8e7dc1a6f2 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/informereventsource/InformerEventSourceTestCustomReconciler.java @@ -14,13 +14,11 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - /** * Copies the config map value from spec into status. The main purpose is to test and demonstrate * sample usage of InformerEventSource */ -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class InformerEventSourceTestCustomReconciler implements Reconciler, EventSourceInitializer { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java index f9a34371ce..f868148145 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/maxinterval/MaxIntervalTestReconciler.java @@ -6,11 +6,8 @@ import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER, - reconciliationMaxInterval = @ReconciliationMaxInterval(interval = 50, - timeUnit = TimeUnit.MILLISECONDS)) +@ControllerConfiguration(reconciliationMaxInterval = @ReconciliationMaxInterval(interval = 50, + timeUnit = TimeUnit.MILLISECONDS)) public class MaxIntervalTestReconciler implements Reconciler, TestExecutionInfoProvider { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java index de733ec6db..f4d9236077 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler1.java @@ -8,9 +8,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER, labelSelector = "!version") +@ControllerConfiguration(labelSelector = "!version") public class MultiVersionCRDTestReconciler1 implements Reconciler { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java index 138647029e..b9521bd8d1 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multiversioncrd/MultiVersionCRDTestReconciler2.java @@ -8,11 +8,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration( - finalizerName = NO_FINALIZER, - labelSelector = "version in (v2)") +@ControllerConfiguration(labelSelector = "version in (v2)") public class MultiVersionCRDTestReconciler2 implements Reconciler { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java index 7a7964623f..02b9770acd 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/observedgeneration/ObservedGenerationTestReconciler.java @@ -3,11 +3,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -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.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class ObservedGenerationTestReconciler implements Reconciler { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java index 8ccd7c2160..a5273aac4c 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/retry/RetryTestCustomReconciler.java @@ -5,7 +5,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -16,8 +15,6 @@ public class RetryTestCustomReconciler implements Reconciler, TestExecutionInfoProvider { - public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName(RetryTestCustomResource.class); private static final Logger log = LoggerFactory.getLogger(RetryTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @@ -34,9 +31,6 @@ public UpdateControl reconcile(RetryTestCustomResource Context context) { numberOfExecutions.addAndGet(1); - if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { - throw new IllegalStateException("Finalizer is not present."); - } log.info("Value: " + resource.getSpec().getValue()); if (numberOfExecutions.get() < numberOfExecutionFails.get() + 1) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java index 146c96c7db..78db3b16f7 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/simple/TestReconciler.java @@ -19,7 +19,8 @@ @ControllerConfiguration(generationAwareEventProcessing = false) public class TestReconciler - implements Reconciler, TestExecutionInfoProvider, + implements Reconciler, Cleaner, + TestExecutionInfoProvider, KubernetesClientAware { private static final Logger log = LoggerFactory.getLogger(TestReconciler.class); @@ -28,21 +29,14 @@ public class TestReconciler ReconcilerUtils.getDefaultFinalizerName(TestCustomResource.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0); private KubernetesClient kubernetesClient; private boolean updateStatus; - public TestReconciler() { - this(true); - } - public TestReconciler(boolean updateStatus) { this.updateStatus = updateStatus; } - public boolean isUpdateStatus() { - return updateStatus; - } - public void setUpdateStatus(boolean updateStatus) { this.updateStatus = updateStatus; } @@ -60,6 +54,7 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { @Override public DeleteControl cleanup( TestCustomResource resource, Context context) { + numberOfCleanupExecutions.incrementAndGet(); Boolean delete = kubernetesClient .configMaps() @@ -139,4 +134,8 @@ private Map configMapData(TestCustomResource resource) { public int getNumberOfExecutions() { return numberOfExecutions.get(); } + + public int getNumberOfCleanupExecutions() { + return numberOfCleanupExecutions.get(); + } } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index e740032d6e..074c454560 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -7,23 +7,14 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.javaoperatorsdk.operator.ReconcilerUtils; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; -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.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.api.reconciler.*; import io.javaoperatorsdk.operator.junit.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) +@ControllerConfiguration public class StandaloneDependentTestReconciler implements Reconciler, EventSourceInitializer, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java index e520e94411..1e76681f25 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/subresource/SubResourceTestCustomReconciler.java @@ -5,7 +5,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; @@ -20,8 +19,6 @@ public class SubResourceTestCustomReconciler public static final int RECONCILER_MIN_EXEC_TIME = 300; - public static final String FINALIZER_NAME = - ReconcilerUtils.getDefaultFinalizerName(SubResourceTestCustomResource.class); private static final Logger log = LoggerFactory.getLogger(SubResourceTestCustomReconciler.class); private final AtomicInteger numberOfExecutions = new AtomicInteger(0); @@ -31,9 +28,6 @@ public class SubResourceTestCustomReconciler public UpdateControl reconcile( SubResourceTestCustomResource resource, Context context) { numberOfExecutions.addAndGet(1); - if (!resource.getMetadata().getFinalizers().contains(FINALIZER_NAME)) { - throw new IllegalStateException("Finalizer is not present."); - } log.info("Value: " + resource.getSpec().getValue()); ensureStatusExists(resource); diff --git a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java index 5ab0596319..0adc9aee09 100644 --- a/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java +++ b/operator-framework/src/test/resources/compile-fixtures/ReconcilerImplemented2Interfaces.java @@ -1,15 +1,13 @@ package io; import io.fabric8.kubernetes.client.CustomResource; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.*; + import java.io.Serializable; @ControllerConfiguration -public class ReconcilerImplemented2Interfaces implements Serializable, Reconciler { +public class ReconcilerImplemented2Interfaces implements Serializable, + Reconciler, Cleaner { public static class MyCustomResource extends CustomResource { } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index b52d7931da..f306292ff8 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -9,12 +9,10 @@ import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource; import io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME; import static java.lang.String.format; -// todo handle this, should work with finalizer -@ControllerConfiguration(finalizerName = NO_FINALIZER, +@ControllerConfiguration( dependents = { @Dependent(type = SecretDependentResource.class), @Dependent(type = SchemaDependentResource.class) 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 273cd086a1..b3155f9199 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SchemaDependentResource.java @@ -11,10 +11,9 @@ import io.fabric8.kubernetes.api.model.Secret; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResourceConfigurator; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.processing.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; import io.javaoperatorsdk.operator.sample.MySQLDbConfig; import io.javaoperatorsdk.operator.sample.MySQLSchema; @@ -29,8 +28,10 @@ public class SchemaDependentResource extends PerResourcePollingDependentResource implements EventSourceProvider, DependentResourceConfigurator, - Creator, - Deleter { + Creator +// todo fix +// , Deleter +{ private static final Logger log = LoggerFactory.getLogger(SchemaDependentResource.class); @@ -74,7 +75,7 @@ private Connection getConnection() throws SQLException { return DriverManager.getConnection(connectURL, dbConfig.getUser(), dbConfig.getPassword()); } - @Override + // @Override public void delete(MySQLSchema primary, Context context) { try (Connection connection = getConnection()) { var userName = primary.getStatus() != null ? primary.getStatus().getUserName() : null; diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 60c4c4d2b0..7ee78deed7 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -7,8 +7,8 @@ import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; 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 b8e8b0566a..59f3bfcd98 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 @@ -39,6 +39,7 @@ class MySQLSchemaOperatorE2E { static final String MY_SQL_NS = "mysql"; private final static List infrastructure = new ArrayList<>(); + public static final String TEST_RESOURCE_NAME = "mydb1"; static { infrastructure.add( @@ -83,25 +84,12 @@ public MySQLSchemaOperatorE2E() throws FileNotFoundException {} @Test void test() throws IOException { // Opening a port-forward if running locally - LocalPortForward portForward = null; - if (isLocal()) { - String podName = - client - .pods() - .inNamespace(MY_SQL_NS) - .withLabel("app", "mysql") - .list() - .getItems() - .get(0) - .getMetadata() - .getName(); - - portForward = client.pods().inNamespace(MY_SQL_NS).withName(podName).portForward(3306, 3306); - } + LocalPortForward portForward = createLocalPortForward(); MySQLSchema testSchema = new MySQLSchema(); testSchema.setMetadata( - new ObjectMetaBuilder().withName("mydb1").withNamespace(operator.getNamespace()).build()); + new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).withNamespace(operator.getNamespace()) + .build()); testSchema.setSpec(new SchemaSpec()); testSchema.getSpec().setEncoding("utf8"); @@ -130,4 +118,22 @@ void test() throws IOException { portForward.close(); } } + + private LocalPortForward createLocalPortForward() { + if (isLocal()) { + String podName = + client + .pods() + .inNamespace(MY_SQL_NS) + .withLabel("app", "mysql") + .list() + .getItems() + .get(0) + .getMetadata() + .getName(); + + return client.pods().inNamespace(MY_SQL_NS).withName(podName).portForward(3306, 3306); + } + return null; + } } diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java index 75632f3e69..1e0c7c08d1 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -5,8 +5,8 @@ import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java index c52553851d..8efaadc0a8 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -5,8 +5,8 @@ import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; public class ServiceDependentResource extends KubernetesDependentResource diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index dfad4533e3..30eb3c3848 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -13,14 +13,11 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - /** * Runs a specified number of Tomcat app server Pods. It uses a Deployment to create the Pods. Also * creates a Service over which the Pods can be accessed. */ @ControllerConfiguration( - finalizerName = NO_FINALIZER, dependents = { @Dependent(type = DeploymentDependentResource.class), @Dependent(type = ServiceDependentResource.class) diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 58d17235ae..68e3fbe135 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -18,23 +18,16 @@ import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; -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.*; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - -@ControllerConfiguration(finalizerName = NO_FINALIZER) -public class WebappReconciler implements Reconciler, EventSourceInitializer { +@ControllerConfiguration +public class WebappReconciler + implements Reconciler, Cleaner, EventSourceInitializer { private static final Logger log = LoggerFactory.getLogger(WebappReconciler.class); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index bc7bb83894..570f2ffb53 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -1,6 +1,8 @@ package io.javaoperatorsdk.operator.sample; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -16,11 +18,8 @@ import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; - /** Shows how to implement reconciler using the low level api directly. */ @ControllerConfiguration( - finalizerName = NO_FINALIZER, labelSelector = WebPageReconciler.LOW_LEVEL_LABEL_KEY) public class WebPageReconciler implements Reconciler, ErrorStatusHandler, EventSourceInitializer { diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java index 90f747d158..7698c3a16d 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java @@ -22,7 +22,8 @@ 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.processing.dependent.kubernetes.CrudKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.processing.event.ResourceID; @@ -30,12 +31,11 @@ import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; /** * Shows how to implement reconciler using standalone dependent resources. */ -@ControllerConfiguration(finalizerName = NO_FINALIZER, +@ControllerConfiguration( labelSelector = WebPageReconcilerDependentResources.DEPENDENT_RESOURCE_LABEL_SELECTOR) public class WebPageReconcilerDependentResources implements Reconciler, ErrorStatusHandler, EventSourceInitializer { @@ -100,7 +100,7 @@ private void createDependentResources(KubernetesClient client) { .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); this.deploymentDR = - new CrudKubernetesDependentResource<>(Deployment.class) { + new CRUDKubernetesDependentResource<>(Deployment.class) { @Override protected Deployment desired(WebPage webPage, Context context) { @@ -132,7 +132,7 @@ protected Deployment desired(WebPage webPage, Context context) { .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); this.serviceDR = - new CrudKubernetesDependentResource<>(Service.class) { + new CRUDKubernetesDependentResource<>(Service.class) { @Override protected Service desired(WebPage webPage, Context context) { @@ -163,7 +163,7 @@ public static String serviceName(WebPage webPage) { } private class ConfigMapDependentResource - extends CrudKubernetesDependentResource + extends CRUKubernetesDependentResource implements PrimaryToSecondaryMapper { public ConfigMapDependentResource() { diff --git a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java index a899c50756..9a5e188cba 100644 --- a/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java +++ b/smoke-test-samples/common/src/main/java/io/javaoperatorsdk/operator/sample/CustomServiceReconciler.java @@ -29,12 +29,6 @@ public CustomServiceReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } - @Override - public DeleteControl cleanup(CustomService resource, Context context) { - log.info("Cleaning up for: {}", resource.getMetadata().getName()); - return Reconciler.super.cleanup(resource, context); - } - @Override public UpdateControl reconcile( CustomService resource, Context context) { @@ -45,17 +39,20 @@ public UpdateControl reconcile( ServiceSpec serviceSpec = new ServiceSpec(); serviceSpec.setPorts(Collections.singletonList(servicePort)); + var service = new ServiceBuilder() + .withNewMetadata() + .withName(resource.getSpec().getName()) + .addToLabels("testLabel", resource.getSpec().getLabel()) + .endMetadata() + .withSpec(serviceSpec) + .build(); + service.addOwnerReference(resource); + kubernetesClient .services() .inNamespace(resource.getMetadata().getNamespace()) - .createOrReplace( - new ServiceBuilder() - .withNewMetadata() - .withName(resource.getSpec().getName()) - .addToLabels("testLabel", resource.getSpec().getLabel()) - .endMetadata() - .withSpec(serviceSpec) - .build()); + .createOrReplace(service); + return UpdateControl.updateResource(resource); } } From efd33e53babfede31ab0d9079f1dcfb4eaa7d4e5 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 21 Mar 2022 10:00:14 +0100 Subject: [PATCH 0368/1608] chore: move version to 3.0.0-SNAPSHOT to reflect API breakage (#1051) --- micrometer-support/pom.xml | 2 +- operator-framework-core/pom.xml | 2 +- operator-framework-junit5/pom.xml | 2 +- operator-framework/pom.xml | 2 +- pom.xml | 2 +- sample-operators/mysql-schema/pom.xml | 2 +- sample-operators/pom.xml | 2 +- sample-operators/tomcat-operator/pom.xml | 2 +- sample-operators/webpage/pom.xml | 2 +- smoke-test-samples/common/pom.xml | 2 +- smoke-test-samples/pom.xml | 2 +- smoke-test-samples/pure-java/pom.xml | 2 +- smoke-test-samples/spring-boot-plain/pom.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/micrometer-support/pom.xml b/micrometer-support/pom.xml index 11f70ef4ca..6bc6606f95 100644 --- a/micrometer-support/pom.xml +++ b/micrometer-support/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml index 79ccc716df..1586f37028 100644 --- a/operator-framework-core/pom.xml +++ b/operator-framework-core/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT ../pom.xml diff --git a/operator-framework-junit5/pom.xml b/operator-framework-junit5/pom.xml index 9f8f4e5c01..7ca3fde2e5 100644 --- a/operator-framework-junit5/pom.xml +++ b/operator-framework-junit5/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT 4.0.0 diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index a624af4e23..5c6c610eac 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -5,7 +5,7 @@ java-operator-sdk io.javaoperatorsdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index e282d0c852..49321f8ff3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.javaoperatorsdk java-operator-sdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT Operator SDK for Java Java SDK for implementing Kubernetes operators pom diff --git a/sample-operators/mysql-schema/pom.xml b/sample-operators/mysql-schema/pom.xml index b2e6aefed9..6fc1d4557f 100644 --- a/sample-operators/mysql-schema/pom.xml +++ b/sample-operators/mysql-schema/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT sample-mysql-schema-operator diff --git a/sample-operators/pom.xml b/sample-operators/pom.xml index 666e471240..d44de9512b 100644 --- a/sample-operators/pom.xml +++ b/sample-operators/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk java-operator-sdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT sample-operators diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index 82385f8c9f..c092ee1b01 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT sample-tomcat-operator diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 93e32953c3..3c07557175 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -7,7 +7,7 @@ io.javaoperatorsdk sample-operators - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT webpage diff --git a/smoke-test-samples/common/pom.xml b/smoke-test-samples/common/pom.xml index 6dcdded08b..a09715d428 100644 --- a/smoke-test-samples/common/pom.xml +++ b/smoke-test-samples/common/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT operator-framework-smoke-test-samples-common diff --git a/smoke-test-samples/pom.xml b/smoke-test-samples/pom.xml index ffbe7c659b..11658f0cf5 100644 --- a/smoke-test-samples/pom.xml +++ b/smoke-test-samples/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT java-operator-sdk-smoke-test-samples diff --git a/smoke-test-samples/pure-java/pom.xml b/smoke-test-samples/pure-java/pom.xml index 21b8162183..c065b05452 100644 --- a/smoke-test-samples/pure-java/pom.xml +++ b/smoke-test-samples/pure-java/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT operator-framework-smoke-test-samples-pure-java diff --git a/smoke-test-samples/spring-boot-plain/pom.xml b/smoke-test-samples/spring-boot-plain/pom.xml index 51860f7af8..4fde68a718 100644 --- a/smoke-test-samples/spring-boot-plain/pom.xml +++ b/smoke-test-samples/spring-boot-plain/pom.xml @@ -6,7 +6,7 @@ io.javaoperatorsdk java-operator-sdk-smoke-test-samples - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT operator-framework-smoke-test-samples-spring-boot From c1467e8fa4c3a3824e6bd5fc640df1fbe0eefb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 21 Mar 2022 14:25:27 +0100 Subject: [PATCH 0369/1608] docs: use anchor headings (#1052) --- docs/_includes/anchor_headings.html | 172 ++++++++++++++++++++++++++++ docs/_layouts/docs.html | 2 +- 2 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 docs/_includes/anchor_headings.html diff --git a/docs/_includes/anchor_headings.html b/docs/_includes/anchor_headings.html new file mode 100644 index 0000000000..f8e22d6a0f --- /dev/null +++ b/docs/_includes/anchor_headings.html @@ -0,0 +1,172 @@ +{% capture headingsWorkspace %} + {% comment %} + Copyright (c) 2018 Vladimir "allejo" Jimenez + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + {% endcomment %} + {% comment %} + Version 1.0.11 + https://github.com/allejo/jekyll-anchor-headings + + "Be the pull request you wish to see in the world." ~Ben Balter + + Usage: + {% include anchor_headings.html html=content anchorBody="#" %} + + Parameters: + * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll + + Optional Parameters: + * beforeHeading (bool) : false - Set to true if the anchor should be placed _before_ the heading's content + * headerAttrs (string) : '' - Any custom HTML attributes that will be added to the heading tag; you may NOT use `id`; + the `%heading%` and `%html_id%` placeholders are available + * anchorAttrs (string) : '' - Any custom HTML attributes that will be added to the `` tag; you may NOT use `href`, `class` or `title`; + the `%heading%` and `%html_id%` placeholders are available + * anchorBody (string) : '' - The content that will be placed inside the anchor; the `%heading%` placeholder is available + * anchorClass (string) : '' - The class(es) that will be used for each anchor. Separate multiple classes with a space + * anchorTitle (string) : '' - The `title` attribute that will be used for anchors + * h_min (int) : 1 - The minimum header level to build an anchor for; any header lower than this value will be ignored + * h_max (int) : 6 - The maximum header level to build an anchor for; any header greater than this value will be ignored + * bodyPrefix (string) : '' - Anything that should be inserted inside of the heading tag _before_ its anchor and content + * bodySuffix (string) : '' - Anything that should be inserted inside of the heading tag _after_ its anchor and content + * generateId (true) : false - Set to true if a header without id should generate an id to use. + + Output: + The original HTML with the addition of anchors inside of all of the h1-h6 headings. + {% endcomment %} + + {% assign minHeader = include.h_min | default: 1 %} + {% assign maxHeader = include.h_max | default: 6 %} + {% assign beforeHeading = include.beforeHeading %} + {% assign headerAttrs = include.headerAttrs %} + {% assign nodes = include.html | split: ' + {% if headerLevel == 0 %} + + {% assign firstChunk = node | split: '>' | first %} + + + {% unless firstChunk contains '<' %} + {% capture node %}{% endcapture %} + {% assign _workspace = node | split: _closingTag %} + {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %} + {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %} + {% assign escaped_header = header | strip_html | strip %} + + {% assign _classWorkspace = _workspace[0] | split: 'class="' %} + {% assign _classWorkspace = _classWorkspace[1] | split: '"' %} + {% assign _html_class = _classWorkspace[0] %} + + {% if _html_class contains "no_anchor" %} + {% assign skip_anchor = true %} + {% else %} + {% assign skip_anchor = false %} + {% endif %} + + {% assign _idWorkspace = _workspace[0] | split: 'id="' %} + {% if _idWorkspace[1] %} + {% assign _idWorkspace = _idWorkspace[1] | split: '"' %} + {% assign html_id = _idWorkspace[0] %} + {% elsif include.generateId %} + + {% assign html_id = escaped_header | slugify %} + {% if html_id == "" %} + {% assign html_id = false %} + {% endif %} + {% capture headerAttrs %}{{ headerAttrs }} id="%html_id%"{% endcapture %} + {% endif %} + + + {% capture anchor %}{% endcapture %} + + {% if skip_anchor == false and html_id and headerLevel >= minHeader and headerLevel <= maxHeader %} + {% if headerAttrs %} + {% capture _hAttrToStrip %}{{ _hAttrToStrip | split: '>' | first }} {{ headerAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}>{% endcapture %} + {% endif %} + + {% capture anchor %}href="#{{ html_id }}"{% endcapture %} + + {% if include.anchorClass %} + {% capture anchor %}{{ anchor }} class="{{ include.anchorClass }}"{% endcapture %} + {% endif %} + + {% if include.anchorTitle %} + {% capture anchor %}{{ anchor }} title="{{ include.anchorTitle | replace: '%heading%', escaped_header }}"{% endcapture %} + {% endif %} + + {% if include.anchorAttrs %} + {% capture anchor %}{{ anchor }} {{ include.anchorAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}{% endcapture %} + {% endif %} + + {% capture anchor %}{{ include.anchorBody | replace: '%heading%', escaped_header | default: '' }}{% endcapture %} + + + {% if beforeHeading %} + {% capture anchor %}{{ anchor }} {% endcapture %} + {% else %} + {% capture anchor %} {{ anchor }}{% endcapture %} + {% endif %} + {% endif %} + + {% capture new_heading %} + + {% endcapture %} + + + {% assign chunkCount = _workspace | size %} + {% if chunkCount > 1 %} + {% capture new_heading %}{{ new_heading }}{{ _workspace | last }}{% endcapture %} + {% endif %} + + {% capture edited_headings %}{{ edited_headings }}{{ new_heading }}{% endcapture %} + {% endfor %} +{% endcapture %}{% assign headingsWorkspace = '' %}{{ edited_headings | strip }} diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html index 1283d701d6..b5cef5c127 100644 --- a/docs/_layouts/docs.html +++ b/docs/_layouts/docs.html @@ -18,7 +18,7 @@

- {{ content }} + {% include anchor_headings.html html=content anchorBody="#" %}
From a6c0784a0f146495360de89e16cb2983383e1c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 21 Mar 2022 14:27:19 +0100 Subject: [PATCH 0370/1608] docs: use anchor headings (#1055) --- docs/_includes/anchor_headings.html | 172 ++++++++++++++++++++++++++++ docs/_layouts/docs.html | 2 +- 2 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 docs/_includes/anchor_headings.html diff --git a/docs/_includes/anchor_headings.html b/docs/_includes/anchor_headings.html new file mode 100644 index 0000000000..f8e22d6a0f --- /dev/null +++ b/docs/_includes/anchor_headings.html @@ -0,0 +1,172 @@ +{% capture headingsWorkspace %} + {% comment %} + Copyright (c) 2018 Vladimir "allejo" Jimenez + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + {% endcomment %} + {% comment %} + Version 1.0.11 + https://github.com/allejo/jekyll-anchor-headings + + "Be the pull request you wish to see in the world." ~Ben Balter + + Usage: + {% include anchor_headings.html html=content anchorBody="#" %} + + Parameters: + * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll + + Optional Parameters: + * beforeHeading (bool) : false - Set to true if the anchor should be placed _before_ the heading's content + * headerAttrs (string) : '' - Any custom HTML attributes that will be added to the heading tag; you may NOT use `id`; + the `%heading%` and `%html_id%` placeholders are available + * anchorAttrs (string) : '' - Any custom HTML attributes that will be added to the `` tag; you may NOT use `href`, `class` or `title`; + the `%heading%` and `%html_id%` placeholders are available + * anchorBody (string) : '' - The content that will be placed inside the anchor; the `%heading%` placeholder is available + * anchorClass (string) : '' - The class(es) that will be used for each anchor. Separate multiple classes with a space + * anchorTitle (string) : '' - The `title` attribute that will be used for anchors + * h_min (int) : 1 - The minimum header level to build an anchor for; any header lower than this value will be ignored + * h_max (int) : 6 - The maximum header level to build an anchor for; any header greater than this value will be ignored + * bodyPrefix (string) : '' - Anything that should be inserted inside of the heading tag _before_ its anchor and content + * bodySuffix (string) : '' - Anything that should be inserted inside of the heading tag _after_ its anchor and content + * generateId (true) : false - Set to true if a header without id should generate an id to use. + + Output: + The original HTML with the addition of anchors inside of all of the h1-h6 headings. + {% endcomment %} + + {% assign minHeader = include.h_min | default: 1 %} + {% assign maxHeader = include.h_max | default: 6 %} + {% assign beforeHeading = include.beforeHeading %} + {% assign headerAttrs = include.headerAttrs %} + {% assign nodes = include.html | split: ' + {% if headerLevel == 0 %} + + {% assign firstChunk = node | split: '>' | first %} + + + {% unless firstChunk contains '<' %} + {% capture node %}{% endcapture %} + {% assign _workspace = node | split: _closingTag %} + {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %} + {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %} + {% assign escaped_header = header | strip_html | strip %} + + {% assign _classWorkspace = _workspace[0] | split: 'class="' %} + {% assign _classWorkspace = _classWorkspace[1] | split: '"' %} + {% assign _html_class = _classWorkspace[0] %} + + {% if _html_class contains "no_anchor" %} + {% assign skip_anchor = true %} + {% else %} + {% assign skip_anchor = false %} + {% endif %} + + {% assign _idWorkspace = _workspace[0] | split: 'id="' %} + {% if _idWorkspace[1] %} + {% assign _idWorkspace = _idWorkspace[1] | split: '"' %} + {% assign html_id = _idWorkspace[0] %} + {% elsif include.generateId %} + + {% assign html_id = escaped_header | slugify %} + {% if html_id == "" %} + {% assign html_id = false %} + {% endif %} + {% capture headerAttrs %}{{ headerAttrs }} id="%html_id%"{% endcapture %} + {% endif %} + + + {% capture anchor %}{% endcapture %} + + {% if skip_anchor == false and html_id and headerLevel >= minHeader and headerLevel <= maxHeader %} + {% if headerAttrs %} + {% capture _hAttrToStrip %}{{ _hAttrToStrip | split: '>' | first }} {{ headerAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}>{% endcapture %} + {% endif %} + + {% capture anchor %}href="#{{ html_id }}"{% endcapture %} + + {% if include.anchorClass %} + {% capture anchor %}{{ anchor }} class="{{ include.anchorClass }}"{% endcapture %} + {% endif %} + + {% if include.anchorTitle %} + {% capture anchor %}{{ anchor }} title="{{ include.anchorTitle | replace: '%heading%', escaped_header }}"{% endcapture %} + {% endif %} + + {% if include.anchorAttrs %} + {% capture anchor %}{{ anchor }} {{ include.anchorAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}{% endcapture %} + {% endif %} + + {% capture anchor %}{{ include.anchorBody | replace: '%heading%', escaped_header | default: '' }}{% endcapture %} + + + {% if beforeHeading %} + {% capture anchor %}{{ anchor }} {% endcapture %} + {% else %} + {% capture anchor %} {{ anchor }}{% endcapture %} + {% endif %} + {% endif %} + + {% capture new_heading %} + + {% endcapture %} + + + {% assign chunkCount = _workspace | size %} + {% if chunkCount > 1 %} + {% capture new_heading %}{{ new_heading }}{{ _workspace | last }}{% endcapture %} + {% endif %} + + {% capture edited_headings %}{{ edited_headings }}{{ new_heading }}{% endcapture %} + {% endfor %} +{% endcapture %}{% assign headingsWorkspace = '' %}{{ edited_headings | strip }} diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html index 1283d701d6..b5cef5c127 100644 --- a/docs/_layouts/docs.html +++ b/docs/_layouts/docs.html @@ -18,7 +18,7 @@
- {{ content }} + {% include anchor_headings.html html=content anchorBody="#" %}
From c9c7016a62191b4a637cbf8e08cbfc677709e19c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Mar 2022 07:53:34 +0100 Subject: [PATCH 0371/1608] chore(deps): bump actions/cache from 2.1.7 to 3 (#1062) Bumps [actions/cache](https://github.com/actions/cache) from 2.1.7 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2.1.7...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sonar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 1611625618..8f830a7092 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -25,7 +25,7 @@ jobs: java-version: 17 cache: 'maven' - name: Cache SonarCloud packages - uses: actions/cache@v2.1.7 + uses: actions/cache@v3 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar From baf94e7b58c1d14982f57d681256d9dfd50000b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20CROCQUESEL?= <88554524+scrocquesel@users.noreply.github.com> Date: Tue, 22 Mar 2022 10:44:16 +0100 Subject: [PATCH 0372/1608] fix: measure total execution time in metrics (#1059) * fix: measure total time execution (reconcile/cleanup) * refactor: optimize controller metrics call --- .../operator/processing/Controller.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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 7ee63d760f..f0aea0e1e2 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 @@ -2,6 +2,7 @@ import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -54,6 +55,8 @@ public class Controller

implements Reconciler

, Cleaner private final boolean contextInitializer; private final boolean hasDeleterDependents; private final boolean isCleaner; + private final Metrics metrics; + public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -61,6 +64,8 @@ public Controller(Reconciler

reconciler, this.reconciler = reconciler; this.configuration = configuration; this.kubernetesClient = kubernetesClient; + this.metrics = Optional.ofNullable(ConfigurationServiceProvider.instance().getMetrics()) + .orElse(Metrics.NOOP); contextInitializer = reconciler instanceof ContextInitializer; eventSourceManager = new EventSourceManager<>(this); @@ -105,9 +110,8 @@ private void initContextIfNeeded(P resource, Context

context) { @Override public DeleteControl cleanup(P resource, Context

context) { - initContextIfNeeded(resource, context); try { - return metrics() + return metrics .timeControllerExecution( new ControllerExecution<>() { @Override @@ -127,6 +131,7 @@ public String successTypeName(DeleteControl deleteControl) { @Override public DeleteControl execute() { + initContextIfNeeded(resource, context); if (hasDeleterDependents) { dependents.stream() .filter(d -> d instanceof Deleter) @@ -147,8 +152,7 @@ public DeleteControl execute() { @Override public UpdateControl

reconcile(P resource, Context

context) throws Exception { - initContextIfNeeded(resource, context); - return metrics().timeControllerExecution( + return metrics.timeControllerExecution( new ControllerExecution<>() { @Override public String name() { @@ -174,18 +178,13 @@ public String successTypeName(UpdateControl

result) { @Override public UpdateControl

execute() throws Exception { + initContextIfNeeded(resource, context); dependents.forEach(dependent -> dependent.reconcile(resource, context)); return reconciler.reconcile(resource, context); } }); } - - private Metrics metrics() { - final var metrics = ConfigurationServiceProvider.instance().getMetrics(); - return metrics != null ? metrics : Metrics.NOOP; - } - @Override public List prepareEventSources(EventSourceContext

context) { List sources = new LinkedList<>(); From 7245f55a06955c45e0c379013a71a1dbdfdad312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 22 Mar 2022 11:23:52 +0100 Subject: [PATCH 0373/1608] fix: Check CRD default to false (#1063) --- .../operator/api/config/ConfigurationService.java | 2 +- .../operator/api/config/Utils.java | 2 +- .../config/ConfigurationServiceProviderTest.java | 6 +++--- .../operator/api/config/UtilsTest.java | 4 ++-- .../operator/processing/ControllerTest.java | 15 --------------- 5 files changed, 7 insertions(+), 22 deletions(-) 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 f13ae118f8..e2f3ac77c9 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 @@ -77,7 +77,7 @@ default Config getClientConfiguration() { * @return {@code true} if CRDs should be checked (default), {@code false} otherwise */ default boolean checkCRDAndValidateLocalModel() { - return true; + return false; } int DEFAULT_RECONCILIATION_THREADS_NUMBER = 5; 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 db882ae09e..d74d27f2d4 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 @@ -64,7 +64,7 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, true); + return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, false); } public static boolean debugThreadPool() { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java index f69dd0bc29..f8c1a0f6aa 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java @@ -35,16 +35,16 @@ void shouldProvideTheSetInstanceIfProvided() { @Test void shouldBePossibleToOverrideConfigOnce() { final var config = new AbstractConfigurationService(null); - assertTrue(config.checkCRDAndValidateLocalModel()); + assertFalse(config.checkCRDAndValidateLocalModel()); ConfigurationServiceProvider.set(config); var instance = ConfigurationServiceProvider.instance(); assertEquals(config, instance); - ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(false)); + ConfigurationServiceProvider.overrideCurrent(o -> o.checkingCRDAndValidateLocalModel(true)); instance = ConfigurationServiceProvider.instance(); assertNotEquals(config, instance); - assertFalse(instance.checkCRDAndValidateLocalModel()); + assertTrue(instance.checkCRDAndValidateLocalModel()); assertThrows(IllegalStateException.class, () -> ConfigurationServiceProvider.overrideCurrent(o -> o.withCloseClientOnStop(false))); 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 0a24d4f26d..75d263a4e1 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 @@ -20,8 +20,8 @@ class UtilsTest { @Test - void shouldCheckCRDAndValidateLocalModelByDefault() { - assertTrue(Utils.shouldCheckCRDAndValidateLocalModel()); + void shouldNotCheckCRDAndValidateLocalModelByDefault() { + assertFalse(Utils.shouldCheckCRDAndValidateLocalModel()); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index 2f5b9bf4ba..1fb5b06562 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import io.fabric8.kubernetes.api.model.Secret; -import io.javaoperatorsdk.operator.MissingCRDException; import io.javaoperatorsdk.operator.MockKubernetesClient; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; @@ -12,7 +11,6 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @SuppressWarnings("unchecked") @@ -47,19 +45,6 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { } } - @Test - void crdShouldBeCheckedForCustomResourcesByDefault() { - ConfigurationServiceProvider.reset(); - final var client = MockKubernetesClient.client(TestCustomResource.class); - when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); - - final var controller = new Controller(reconciler, configuration, client); - // since we're not really connected to a cluster and the CRD wouldn't be deployed anyway, we - // expect a MissingCRDException to be thrown - assertThrows(MissingCRDException.class, controller::start); - verify(client, times(1)).apiextensions(); - } - @Test void usesFinalizerIfThereIfReconcilerImplementsCleaner() { Reconciler reconciler = mock(Reconciler.class, withSettings().extraInterfaces(Cleaner.class)); From cb1fa110f3333bb9dda2b0d10c637cb28b3ea8cb Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Tue, 22 Mar 2022 12:38:40 +0100 Subject: [PATCH 0374/1608] feat: add managed dependent webpage reconciler implementation (#1050) * feat: give explicit access to client from KubernetesClientAware * refactor: WebPageReconcilerDependentResources -> WebPageStandaloneDependentsReconciler * refactor: extract methods into Utils, extract dependent resources * fix: make Utils.simulateErrorIfRequested throw exception * feat: add managed dependent reconciler implementation * fix: unify artifactId --- .../managed/KubernetesClientAware.java | 2 + .../KubernetesDependentResource.java | 5 + sample-operators/webpage/pom.xml | 2 +- .../sample/ConfigMapDependentResource.java | 65 ++++++ .../sample/DeploymentDependentResource.java | 45 ++++ .../sample/ErrorSimulationException.java | 2 +- .../sample/ServiceDependentResource.java | 33 +++ .../operator/sample/Utils.java | 40 ++++ .../WebPageManagedDependentsReconciler.java | 48 ++++ .../operator/sample/WebPageOperator.java | 2 +- .../operator/sample/WebPageReconciler.java | 56 +++-- .../WebPageReconcilerDependentResources.java | 206 ------------------ ...WebPageStandaloneDependentsReconciler.java | 92 ++++++++ .../sample/WebPageOperatorAbstractTest.java | 6 +- .../WebPageOperatorDependentResourcesE2E.java | 2 +- 15 files changed, 362 insertions(+), 244 deletions(-) create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java delete mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java create mode 100644 sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java index 0e787753a5..2a28813c35 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/KubernetesClientAware.java @@ -4,4 +4,6 @@ public interface KubernetesClientAware { void setKubernetesClient(KubernetesClient kubernetesClient); + + KubernetesClient getKubernetesClient(); } 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 233deb020d..32606a5ebd 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 @@ -156,6 +156,11 @@ public void setKubernetesClient(KubernetesClient kubernetesClient) { this.client = kubernetesClient; } + @Override + public KubernetesClient getKubernetesClient() { + return client; + } + @Override protected R desired(P primary, Context

context) { return super.desired(primary, context); diff --git a/sample-operators/webpage/pom.xml b/sample-operators/webpage/pom.xml index 3c07557175..08b08e3baa 100644 --- a/sample-operators/webpage/pom.xml +++ b/sample-operators/webpage/pom.xml @@ -10,7 +10,7 @@ 3.0.0-SNAPSHOT - webpage + sample-webpage-operator Operator SDK - Samples - WebPage Provisions an nginx Webserver based on a CRD with give html jar diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java new file mode 100644 index 0000000000..8cacd49765 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ConfigMapDependentResource.java @@ -0,0 +1,65 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; + +import static io.javaoperatorsdk.operator.sample.Utils.configMapName; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; + +// this annotation only activates when using managed dependents and is not otherwise needed +@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) +class ConfigMapDependentResource extends CRUKubernetesDependentResource + implements PrimaryToSecondaryMapper { + + private static final Logger log = LoggerFactory.getLogger(ConfigMapDependentResource.class); + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(WebPage webPage, Context context) { + Map data = new HashMap<>(); + data.put("index.html", webPage.getSpec().getHtml()); + return new ConfigMapBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(configMapName(webPage)) + .withNamespace(webPage.getMetadata().getNamespace()) + .build()) + .withData(data) + .build(); + } + + @Override + public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, + Context context) { + var res = super.update(actual, target, primary, context); + var ns = actual.getMetadata().getNamespace(); + log.info("Restarting pods because HTML has changed in {}", + ns); + getKubernetesClient() + .pods() + .inNamespace(ns) + .withLabel("app", deploymentName(primary)) + .delete(); + return res; + } + + @Override + public ResourceID associatedSecondaryID(WebPage primary) { + return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java new file mode 100644 index 0000000000..0fd907f245 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/DeploymentDependentResource.java @@ -0,0 +1,45 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.sample.Utils.configMapName; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; + +// this annotation only activates when using managed dependents and is not otherwise needed +@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) +class DeploymentDependentResource extends CRUDKubernetesDependentResource { + + public DeploymentDependentResource() { + super(Deployment.class); + } + + @Override + protected Deployment desired(WebPage webPage, Context context) { + var deploymentName = deploymentName(webPage); + Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); + deployment.getMetadata().setName(deploymentName); + deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); + + deployment + .getSpec() + .getTemplate() + .getMetadata() + .getLabels() + .put("app", deploymentName); + deployment + .getSpec() + .getTemplate() + .getSpec() + .getVolumes() + .get(0) + .setConfigMap( + new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); + return deployment; + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java index e2d3f3c1dd..94efc05baa 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ErrorSimulationException.java @@ -1,6 +1,6 @@ package io.javaoperatorsdk.operator.sample; -public class ErrorSimulationException extends RuntimeException { +public class ErrorSimulationException extends Exception { public ErrorSimulationException(String message) { super(message); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java new file mode 100644 index 0000000000..cb06ab594b --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ServiceDependentResource.java @@ -0,0 +1,33 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.Service; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.Utils.serviceName; + +// this annotation only activates when using managed dependents and is not otherwise needed +@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR) +class ServiceDependentResource extends CRUDKubernetesDependentResource { + + public ServiceDependentResource() { + super(Service.class); + } + + @Override + protected Service desired(WebPage webPage, Context context) { + Service service = loadYaml(Service.class, getClass(), "service.yaml"); + service.getMetadata().setName(serviceName(webPage)); + service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); + Map labels = new HashMap<>(); + labels.put("app", deploymentName(webPage)); + service.getSpec().setSelector(labels); + return service; + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java new file mode 100644 index 0000000000..f08c78f5db --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/Utils.java @@ -0,0 +1,40 @@ +package io.javaoperatorsdk.operator.sample; + +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; + +public class Utils { + + private Utils() {} + + static WebPageStatus createStatus(String configMapName) { + WebPageStatus status = new WebPageStatus(); + status.setHtmlConfigMap(configMapName); + status.setAreWeGood(true); + status.setErrorMessage(null); + return status; + } + + static String configMapName(WebPage nginx) { + return nginx.getMetadata().getName() + "-html"; + } + + static String deploymentName(WebPage nginx) { + return nginx.getMetadata().getName(); + } + + static String serviceName(WebPage webPage) { + return webPage.getMetadata().getName(); + } + + static ErrorStatusUpdateControl handleError(WebPage resource, Exception e) { + resource.getStatus().setErrorMessage("Error: " + e.getMessage()); + return ErrorStatusUpdateControl.updateStatus(resource); + } + + static void simulateErrorIfRequested(WebPage webPage) throws ErrorSimulationException { + if (webPage.getSpec().getHtml().contains("error")) { + // special case just to showcase error if doing a demo + throw new ErrorSimulationException("Simulating error"); + } + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java new file mode 100644 index 0000000000..f257893f8c --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java @@ -0,0 +1,48 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +import static io.javaoperatorsdk.operator.sample.Utils.createStatus; +import static io.javaoperatorsdk.operator.sample.Utils.handleError; +import static io.javaoperatorsdk.operator.sample.Utils.simulateErrorIfRequested; +import static io.javaoperatorsdk.operator.sample.WebPageManagedDependentsReconciler.SELECTOR; + +/** + * Shows how to implement a reconciler with managed dependent resources. + */ +@ControllerConfiguration( + labelSelector = SELECTOR, + dependents = { + @Dependent(type = ConfigMapDependentResource.class), + @Dependent(type = DeploymentDependentResource.class), + @Dependent(type = ServiceDependentResource.class) + }) +public class WebPageManagedDependentsReconciler + implements Reconciler, ErrorStatusHandler { + + static final String SELECTOR = "managed"; + + @Override + public ErrorStatusUpdateControl updateErrorStatus(WebPage resource, + Context context, Exception e) { + return handleError(resource, e); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) + throws Exception { + simulateErrorIfRequested(webPage); + + final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow() + .getMetadata().getName(); + webPage.setStatus(createStatus(name)); + return UpdateControl.updateStatus(webPage); + } +} 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 0277da8199..30958dbc84 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 @@ -30,7 +30,7 @@ public static void main(String[] args) throws IOException { if (WEBPAGE_RECONCILER_ENV_VALUE.equals(System.getenv(WEBPAGE_RECONCILER_ENV))) { operator.register(new WebPageReconciler(client)); } else { - operator.register(new WebPageReconcilerDependentResources(client)); + operator.register(new WebPageStandaloneDependentsReconciler(client)); } operator.installShutdownHook(); operator.start(); diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java index 570f2ffb53..3d74dcb00b 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconciler.java @@ -8,16 +8,33 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.ReconcilerUtils; 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.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +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.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import static io.javaoperatorsdk.operator.sample.Utils.configMapName; +import static io.javaoperatorsdk.operator.sample.Utils.createStatus; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.Utils.handleError; +import static io.javaoperatorsdk.operator.sample.Utils.serviceName; +import static io.javaoperatorsdk.operator.sample.Utils.simulateErrorIfRequested; + /** Shows how to implement reconciler using the low level api directly. */ @ControllerConfiguration( labelSelector = WebPageReconciler.LOW_LEVEL_LABEL_KEY) @@ -35,11 +52,9 @@ public WebPageReconciler(KubernetesClient kubernetesClient) { this.kubernetesClient = kubernetesClient; } - InformerEventSource configMapEventSource; - @Override public List prepareEventSources(EventSourceContext context) { - configMapEventSource = + var configMapEventSource = new InformerEventSource<>(InformerConfiguration.from(context, ConfigMap.class) .withLabelSelector(LOW_LEVEL_LABEL_KEY) .build(), context); @@ -55,11 +70,11 @@ public List prepareEventSources(EventSourceContext context } @Override - public UpdateControl reconcile(WebPage webPage, Context context) { + public UpdateControl reconcile(WebPage webPage, Context context) + throws Exception { log.info("Reconciling web page: {}", webPage); - if (webPage.getSpec().getHtml().contains("error")) { - throw new ErrorSimulationException("Simulating error"); - } + simulateErrorIfRequested(webPage); + String ns = webPage.getMetadata().getNamespace(); String configMapName = configMapName(webPage); String deploymentName = deploymentName(webPage); @@ -107,14 +122,6 @@ public UpdateControl reconcile(WebPage webPage, Context contex return UpdateControl.updateStatus(webPage); } - private WebPageStatus createStatus(String configMapName) { - WebPageStatus status = new WebPageStatus(); - status.setHtmlConfigMap(configMapName); - status.setAreWeGood(true); - status.setErrorMessage(null); - return status; - } - private boolean match(Deployment desiredDeployment, Deployment deployment) { if (deployment == null) { return false; @@ -196,22 +203,9 @@ public static Map lowLevelLabel() { return labels; } - private static String configMapName(WebPage nginx) { - return nginx.getMetadata().getName() + "-html"; - } - - private static String deploymentName(WebPage nginx) { - return nginx.getMetadata().getName(); - } - - private static String serviceName(WebPage nginx) { - return nginx.getMetadata().getName(); - } - @Override public ErrorStatusUpdateControl updateErrorStatus( WebPage resource, Context context, Exception e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return ErrorStatusUpdateControl.updateStatus(resource); + return handleError(resource, e); } } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java deleted file mode 100644 index 7698c3a16d..0000000000 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageReconcilerDependentResources.java +++ /dev/null @@ -1,206 +0,0 @@ -package io.javaoperatorsdk.operator.sample; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.api.model.ConfigMap; -import io.fabric8.kubernetes.api.model.ConfigMapBuilder; -import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; -import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.apps.Deployment; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; -import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; -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.processing.dependent.kubernetes.CRUDKubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; -import io.javaoperatorsdk.operator.processing.event.ResourceID; -import io.javaoperatorsdk.operator.processing.event.source.EventSource; -import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; - -import static io.javaoperatorsdk.operator.ReconcilerUtils.loadYaml; - -/** - * Shows how to implement reconciler using standalone dependent resources. - */ -@ControllerConfiguration( - labelSelector = WebPageReconcilerDependentResources.DEPENDENT_RESOURCE_LABEL_SELECTOR) -public class WebPageReconcilerDependentResources - implements Reconciler, ErrorStatusHandler, EventSourceInitializer { - - public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level"; - private static final Logger log = - LoggerFactory.getLogger(WebPageReconcilerDependentResources.class); - private final KubernetesClient kubernetesClient; - - private KubernetesDependentResource configMapDR; - private KubernetesDependentResource deploymentDR; - private KubernetesDependentResource serviceDR; - - public WebPageReconcilerDependentResources(KubernetesClient kubernetesClient) { - this.kubernetesClient = kubernetesClient; - createDependentResources(kubernetesClient); - } - - @Override - public List prepareEventSources(EventSourceContext context) { - return List.of( - configMapDR.initEventSource(context), - deploymentDR.initEventSource(context), - serviceDR.initEventSource(context)); - } - - @Override - public UpdateControl reconcile(WebPage webPage, Context context) { - if (webPage.getSpec().getHtml().contains("error")) { - // special case just to showcase error if doing a demo - throw new ErrorSimulationException("Simulating error"); - } - - configMapDR.reconcile(webPage, context); - deploymentDR.reconcile(webPage, context); - serviceDR.reconcile(webPage, context); - - webPage.setStatus( - createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); - return UpdateControl.updateStatus(webPage); - } - - private WebPageStatus createStatus(String configMapName) { - WebPageStatus status = new WebPageStatus(); - status.setHtmlConfigMap(configMapName); - status.setAreWeGood(true); - status.setErrorMessage(null); - return status; - } - - @Override - public ErrorStatusUpdateControl updateErrorStatus( - WebPage resource, Context retryInfo, Exception e) { - resource.getStatus().setErrorMessage("Error: " + e.getMessage()); - return ErrorStatusUpdateControl.updateStatus(resource); - } - - private void createDependentResources(KubernetesClient client) { - this.configMapDR = new ConfigMapDependentResource(); - this.configMapDR.setKubernetesClient(client); - configMapDR.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); - - this.deploymentDR = - new CRUDKubernetesDependentResource<>(Deployment.class) { - - @Override - protected Deployment desired(WebPage webPage, Context context) { - var deploymentName = deploymentName(webPage); - Deployment deployment = loadYaml(Deployment.class, getClass(), "deployment.yaml"); - deployment.getMetadata().setName(deploymentName); - deployment.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - deployment.getSpec().getSelector().getMatchLabels().put("app", deploymentName); - - deployment - .getSpec() - .getTemplate() - .getMetadata() - .getLabels() - .put("app", deploymentName); - deployment - .getSpec() - .getTemplate() - .getSpec() - .getVolumes() - .get(0) - .setConfigMap( - new ConfigMapVolumeSourceBuilder().withName(configMapName(webPage)).build()); - return deployment; - } - }; - deploymentDR.setKubernetesClient(client); - deploymentDR.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); - - this.serviceDR = - new CRUDKubernetesDependentResource<>(Service.class) { - - @Override - protected Service desired(WebPage webPage, Context context) { - Service service = loadYaml(Service.class, getClass(), "service.yaml"); - service.getMetadata().setName(serviceName(webPage)); - service.getMetadata().setNamespace(webPage.getMetadata().getNamespace()); - Map labels = new HashMap<>(); - labels.put("app", deploymentName(webPage)); - service.getSpec().setSelector(labels); - return service; - } - }; - serviceDR.setKubernetesClient(client); - serviceDR.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); - } - - public static String configMapName(WebPage nginx) { - return nginx.getMetadata().getName() + "-html"; - } - - public static String deploymentName(WebPage nginx) { - return nginx.getMetadata().getName(); - } - - public static String serviceName(WebPage webPage) { - return webPage.getMetadata().getName(); - } - - private class ConfigMapDependentResource - extends CRUKubernetesDependentResource - implements PrimaryToSecondaryMapper { - - public ConfigMapDependentResource() { - super(ConfigMap.class); - } - - @Override - protected ConfigMap desired(WebPage webPage, Context context) { - Map data = new HashMap<>(); - data.put("index.html", webPage.getSpec().getHtml()); - return new ConfigMapBuilder() - .withMetadata( - new ObjectMetaBuilder() - .withName(WebPageReconcilerDependentResources.configMapName(webPage)) - .withNamespace(webPage.getMetadata().getNamespace()) - .build()) - .withData(data) - .build(); - } - - @Override - public ConfigMap update(ConfigMap actual, ConfigMap target, WebPage primary, - Context context) { - var res = super.update(actual, target, primary, context); - var ns = actual.getMetadata().getNamespace(); - log.info("Restarting pods because HTML has changed in {}", ns); - kubernetesClient - .pods() - .inNamespace(ns) - .withLabel("app", deploymentName(primary)) - .delete(); - return res; - } - - @Override - public ResourceID associatedSecondaryID(WebPage primary) { - return new ResourceID(configMapName(primary), primary.getMetadata().getNamespace()); - } - } -} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java new file mode 100644 index 0000000000..180c6ade65 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageStandaloneDependentsReconciler.java @@ -0,0 +1,92 @@ +package io.javaoperatorsdk.operator.sample; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +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.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; + +import static io.javaoperatorsdk.operator.sample.Utils.createStatus; +import static io.javaoperatorsdk.operator.sample.Utils.handleError; +import static io.javaoperatorsdk.operator.sample.Utils.simulateErrorIfRequested; + +/** + * Shows how to implement reconciler using standalone dependent resources. + */ +@ControllerConfiguration( + labelSelector = WebPageStandaloneDependentsReconciler.DEPENDENT_RESOURCE_LABEL_SELECTOR) +public class WebPageStandaloneDependentsReconciler + implements Reconciler, ErrorStatusHandler, EventSourceInitializer { + + public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level"; + private static final Logger log = + LoggerFactory.getLogger(WebPageStandaloneDependentsReconciler.class); + + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; + + public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) { + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + return List.of( + configMapDR.initEventSource(context), + deploymentDR.initEventSource(context), + serviceDR.initEventSource(context)); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) + throws Exception { + simulateErrorIfRequested(webPage); + + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); + + webPage.setStatus( + createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); + return UpdateControl.updateStatus(webPage); + } + + @Override + public ErrorStatusUpdateControl updateErrorStatus( + WebPage resource, Context retryInfo, Exception e) { + return handleError(resource, e); + } + + private void createDependentResources(KubernetesClient client) { + this.configMapDR = new ConfigMapDependentResource(); + this.configMapDR.setKubernetesClient(client); + configMapDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.deploymentDR = new DeploymentDependentResource(); + deploymentDR.setKubernetesClient(client); + deploymentDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + + this.serviceDR = new ServiceDependentResource(); + serviceDR.setKubernetesClient(client); + serviceDR.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + } +} diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java index 092ad6f25a..65a3084989 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorAbstractTest.java @@ -20,7 +20,8 @@ import io.fabric8.kubernetes.client.LocalPortForward; import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension; -import static io.javaoperatorsdk.operator.sample.WebPageReconcilerDependentResources.serviceName; +import static io.javaoperatorsdk.operator.sample.Utils.deploymentName; +import static io.javaoperatorsdk.operator.sample.Utils.serviceName; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -50,8 +51,7 @@ void testAddingWebPage() { .until( () -> { var actual = operator().get(WebPage.class, TEST_PAGE); - var deployment = operator().get(Deployment.class, - WebPageReconcilerDependentResources.deploymentName(webPage)); + var deployment = operator().get(Deployment.class, deploymentName(webPage)); return Boolean.TRUE.equals(actual.getStatus().getAreWeGood()) && Objects.equals(deployment.getSpec().getReplicas(), diff --git a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java index 3bfdd3f45b..df024b890a 100644 --- a/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java +++ b/sample-operators/webpage/src/test/java/io/javaoperatorsdk/operator/sample/WebPageOperatorDependentResourcesE2E.java @@ -18,7 +18,7 @@ public WebPageOperatorDependentResourcesE2E() throws FileNotFoundException {} isLocal() ? OperatorExtension.builder() .waitForNamespaceDeletion(false) - .withReconciler(new WebPageReconcilerDependentResources(client)) + .withReconciler(new WebPageStandaloneDependentsReconciler(client)) .build() : E2EOperatorExtension.builder() .waitForNamespaceDeletion(false) From 76650a5201816912523a9c4eca51dc74069a2b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 22 Mar 2022 12:40:48 +0100 Subject: [PATCH 0375/1608] docs: dependent resources (#1026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: dependent resources * docs: dr motivation * docs: add flow chart to motivation, detail design * fix: remove now fixed fix-me :) [skip-ci] * fix: missing word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: re-order sentences Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: clarify Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: remove extra word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: header * fix: simplify dependent logic [skip-ci] * fix: reword [skip ci] Co-authored-by: Attila Mészáros Co-authored-by: Chris Laprun Co-authored-by: Chris Laprun Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> --- docs/_data/sidebar.yml | 2 + docs/documentation/dependent-resources.md | 117 ++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 docs/documentation/dependent-resources.md diff --git a/docs/_data/sidebar.yml b/docs/_data/sidebar.yml index 3f3f1734b7..fe27a8e6aa 100644 --- a/docs/_data/sidebar.yml +++ b/docs/_data/sidebar.yml @@ -9,6 +9,8 @@ url: /docs/glossary - title: Features url: /docs/features + - title: Dependent Resource Feature + url: /docs/dependent-resources - title: Patterns and Best Practices url: /docs/patterns-best-practices - title: FAQ diff --git a/docs/documentation/dependent-resources.md b/docs/documentation/dependent-resources.md new file mode 100644 index 0000000000..225c5a4377 --- /dev/null +++ b/docs/documentation/dependent-resources.md @@ -0,0 +1,117 @@ +--- +title: Dependent Resources Feature +description: Dependent Resources Feature +layout: docs +permalink: /docs/dependent-resources +--- + +# Dependent Resources + +DISCLAIMER: The Dependent Resource support is relatively new and, while we strove to cover what we +anticipate will be the most common use cases, the implementation is not simple and might still +evolve. As a result, some APIs could be still a subject of change in the future. However, +non-backwards compatible changes are expected to be trivial to adapt to. + +## Motivations and Goals + +Most operators need to deal with secondary resources when trying to realize the desired state +described by the primary resource it is in charge of. For example, the Kubernetes-native +`Deployment` controller needs to manage `ReplicaSet` instances as part of a `Deployment`'s +reconciliation process. In this instance, `ReplicatSet` is considered a secondary resource for +the `Deployment` controller. + +Controllers that deal with secondary resources typically need to perform the following steps, for +each secondary resource: + +```mermaid +flowchart TD + +compute[Compute desired secondary resource based on primary state] --> A +A{Secondary resource exists?} +A -- Yes --> match +A -- No --> Create --> Done + +match{Matches desired state as defined by primary?} +match -- Yes --> Done +match -- No --> Update --> Done + +``` + +While these steps are not difficult in and of themselves, there are some subtleties that can lead to +bugs or sub-optimal code if not done right. As this process is pretty much similar for each +dependent resource, it makes sense for the SDK to offer some level of support to remove the +boilerplate code of these repetitive actions. It should be possible to handle common cases (such as +dealing with Kubernetes-native secondary resources) in a semi-declarative way with only a minimal +amount of code, JOSDK taking care of wiring everything accordingly. + +Moreover, in order for your reconciler to get informed of events on these secondary resources, you +need to configure and create event sources and maintain them. JOSDK already makes it rather easy to +deal with these, but dependent resources makes it even simpler. + +Finally, there are also opportunities for the SDK to transparently add features that are even +trickier to get right, such as immediate caching of updated or created resources (so that your +reconciler doesn't need to wait for a cluster roundtrip to continue its work) and associated event +filtering (so that something your reconciler just changed doesn't re-trigger a reconciliation, for +example). + +## Design + +### `DependentResource` vs. `AbstractDependentResource` + +The new `DependentResource` interface lies at the core of the design and strives to encapsulate the +logic that is required to reconcile the state of the associated secondary resource based on the +state of the primary one. For most cases, this logic will follow the flow expressed above and JOSDK +provides a very convenient implementation of this logic in the form of the +`AbstractDependentResource` class. If your logic doesn't fit this pattern, though, you can still +provide your own `reconcile` method implementation. While the benefits of using dependent resources +are less obvious in that case, this allows you to separate the logic necessary to deal with each +secondary resource in its own class that can then be tested in isolation via unit tests. You can +also use the declarative support with your own implementations as we shall see later on. + +`AbstractDependentResource` is designed so that classes extending it specify which functionality +they support by implementing trait interfaces. This design has been selected to express the fact +that not all secondary resources are completely under the control of the primary reconciler: some +dependent resources are only ever created or updated for example and we needed a way to let JOSDK +know when that is the case. We therefore provide trait interfaces: `Creator`, +`Updater` and `Deleter` to express that the `DependentResource` implementation will provide custom +functionality to create, update and delete its associated secondary resources, respectively. If +these traits are not implemented then parts of the logic described above is never triggered: if your +implementation doesn't implement `Creator`, for example, +`AbstractDependentResource` will never try to create the associated secondary resource, even if it +doesn't exist. It is possible to not implement any of these traits and therefore create read-only dependent resources that will trigger your +reconciler whenever a user interacts with them but that are never modified by your reconciler +itself. + +### Batteries included: convenient `DependentResource` implementations! + +JOSDK also offers several other convenient implementations building on top of +`AbstractDependentResource` that you can use as starting points for your own implementations. + +One such implementation is the `KubernetesDependentResource` class that makes it really easy to work +with Kubernetes-native resources. In this case, you usually only need to provide an +implementation for the `desired` method to tell JOSDK what the desired state of your secondary +resource should be based on the specified primary resource state. JOSDK takes care of everything +else using default implementations that you can override in case you need more precise control of +what's going on. + +We also provide implementations that makes it very easy to cache +(`AbstractCachingDependentResource`) or make it easy to poll for changes in external +resources (`PollingDependentResource`, `PerResourcePollingDependentResource`). All the provided +implementations can be found in the `io/javaoperatorsdk/operator/processing/dependent` package of +the `operator-framework-core` module. + +## Managed Dependent Resources + +As mentioned previously, one goal of this implementation is to make it possible to +semi-declaratively create and wire dependent resources. You can annotate your reconciler with +`@Dependent` annotations that specify which `DependentResource` implementation it depends upon. +JOSDK will take the appropriate steps to wire everything together and call your +`DependentResource` implementations `reconcile` method before your primary resource is reconciled. +This makes sense in most use cases where the logic associated with the primary resource is usually +limited to status handling based on the state of the secondary resources. This behavior and +automated handling is referred to as "managed" because the `DependentResource` +implementations are managed by JOSDK. + +## Standalone Dependent Resources + +## Other Dependent Resources features \ No newline at end of file From d4df638dbc5e92947b0cbb42b84b52057e95d46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 22 Mar 2022 12:54:01 +0100 Subject: [PATCH 0376/1608] fix: set default CRD check to false (#1057) --- .../operator/api/config/ConfigurationService.java | 2 +- .../java/io/javaoperatorsdk/operator/api/config/Utils.java | 2 +- .../io/javaoperatorsdk/operator/api/config/UtilsTest.java | 2 +- .../javaoperatorsdk/operator/processing/ControllerTest.java | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) 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 49ccc1da07..dc19d00c56 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 @@ -75,7 +75,7 @@ default Config getClientConfiguration() { * @return {@code true} if CRDs should be checked (default), {@code false} otherwise */ default boolean checkCRDAndValidateLocalModel() { - return true; + return false; } int DEFAULT_RECONCILIATION_THREADS_NUMBER = 5; 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 06bbcaa7a8..10a213ab11 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 @@ -61,7 +61,7 @@ public static boolean isValidateCustomResourcesEnvVarSet() { } public static boolean shouldCheckCRDAndValidateLocalModel() { - return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, true); + return getBooleanFromSystemPropsOrDefault(CHECK_CRD_ENV_KEY, false); } public static boolean debugThreadPool() { 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 a17eb1ba59..1e8468ed1d 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 @@ -8,7 +8,7 @@ class UtilsTest { @Test void shouldCheckCRDAndValidateLocalModelByDefault() { - assertTrue(Utils.shouldCheckCRDAndValidateLocalModel()); + assertFalse(Utils.shouldCheckCRDAndValidateLocalModel()); } @Test diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java index d61b50d583..4c749dcf71 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/ControllerTest.java @@ -54,10 +54,10 @@ void crdShouldNotBeCheckedForCustomResourcesIfDisabled() { } @Test - void crdShouldBeCheckedForCustomResourcesByDefault() { + void crdCanBeCheckedForCustomResources() { final var client = mock(KubernetesClient.class); final var configurationService = mock(ConfigurationService.class); - when(configurationService.checkCRDAndValidateLocalModel()).thenCallRealMethod(); + when(configurationService.checkCRDAndValidateLocalModel()).thenReturn(true); final var reconciler = mock(Reconciler.class); final var configuration = mock(ControllerConfiguration.class); when(configuration.getResourceClass()).thenReturn(TestCustomResource.class); From 8078fec2b5036228ef76be264b037f36b70c5cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 22 Mar 2022 13:00:09 +0100 Subject: [PATCH 0377/1608] docs: mermaid support (#1053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: dependent resources * docs: dr motivation * docs: add flow chart to motivation, detail design * fix: remove now fixed fix-me :) [skip-ci] * fix: missing word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: re-order sentences Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: clarify Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: remove extra word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: header * fix: simplify dependent logic [skip-ci] * fix: reword [skip ci] Co-authored-by: Attila Mészáros * chore: move version to 3.0.0-SNAPSHOT to reflect API breakage (#1051) * docs: mermaid support * fix: mermaid integration * docs: dependent resources * docs: dr motivation * docs: add flow chart to motivation, detail design * fix: remove now fixed fix-me :) [skip-ci] * fix: missing word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: re-order sentences Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: clarify Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: split sentence Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: remove extra word Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> * fix: header * fix: simplify dependent logic [skip-ci] * fix: reword [skip ci] Co-authored-by: Attila Mészáros * fix: diagram * fix: flag not needed Co-authored-by: Chris Laprun Co-authored-by: Chris Laprun Co-authored-by: Sébastien CROCQUESEL <88554524+scrocquesel@users.noreply.github.com> --- docs/_layouts/docs.html | 2 ++ docs/assets/js/mermaid.min.js | 3 +++ docs/assets/js/mermaid.min.js.map | 1 + docs/documentation/dependent-resources.md | 5 +++-- 4 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 docs/assets/js/mermaid.min.js create mode 100644 docs/assets/js/mermaid.min.js.map diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html index b5cef5c127..16db62e0c0 100644 --- a/docs/_layouts/docs.html +++ b/docs/_layouts/docs.html @@ -6,6 +6,7 @@ {% if page.title %}{{ page.title }}{% else %}{{ site.title | escape }}{% endif %} {% include links.html %} {% include analytics.html %} + {% include navbar.html %} @@ -20,6 +21,7 @@

{% include anchor_headings.html html=content anchorBody="#" %}
+ diff --git a/docs/assets/js/mermaid.min.js b/docs/assets/js/mermaid.min.js new file mode 100644 index 0000000000..8f8ef35651 --- /dev/null +++ b/docs/assets/js/mermaid.min.js @@ -0,0 +1,3 @@ +/*! For license information please see mermaid.min.js.LICENSE.txt */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.mermaid=e():t.mermaid=e()}("undefined"!=typeof self?self:this,(function(){return(()=>{var t={1362:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,6],n=[1,7],r=[1,8],i=[1,9],a=[1,12],o=[1,11],s=[1,15,24],c=[1,19],u=[1,31],l=[1,34],h=[1,32],f=[1,33],d=[1,35],p=[1,36],y=[1,37],g=[1,38],m=[1,41],v=[1,42],b=[1,43],_=[1,44],x=[15,24],w=[1,56],k=[1,57],T=[1,58],E=[1,59],C=[1,60],S=[1,61],A=[15,24,31,38,39,47,50,51,52,53,54,55,60,62],M=[15,24,29,31,38,39,43,47,50,51,52,53,54,55,60,62,77,78,79,80],N=[7,8,9,10,15,18,22,24],D=[47,77,78,79,80],O=[47,54,55,77,78,79,80],B=[47,50,51,52,53,77,78,79,80],L=[15,24,31],I=[1,93],R={trace:function(){},yy:{},symbols_:{error:2,start:3,mermaidDoc:4,direction:5,directive:6,direction_tb:7,direction_bt:8,direction_rl:9,direction_lr:10,graphConfig:11,openDirective:12,typeDirective:13,closeDirective:14,NEWLINE:15,":":16,argDirective:17,open_directive:18,type_directive:19,arg_directive:20,close_directive:21,CLASS_DIAGRAM:22,statements:23,EOF:24,statement:25,className:26,alphaNumToken:27,classLiteralName:28,GENERICTYPE:29,relationStatement:30,LABEL:31,classStatement:32,methodStatement:33,annotationStatement:34,clickStatement:35,cssClassStatement:36,CLASS:37,STYLE_SEPARATOR:38,STRUCT_START:39,members:40,STRUCT_STOP:41,ANNOTATION_START:42,ANNOTATION_END:43,MEMBER:44,SEPARATOR:45,relation:46,STR:47,relationType:48,lineType:49,AGGREGATION:50,EXTENSION:51,COMPOSITION:52,DEPENDENCY:53,LINE:54,DOTTED_LINE:55,CALLBACK:56,LINK:57,LINK_TARGET:58,CLICK:59,CALLBACK_NAME:60,CALLBACK_ARGS:61,HREF:62,CSSCLASS:63,commentToken:64,textToken:65,graphCodeTokens:66,textNoTagsToken:67,TAGSTART:68,TAGEND:69,"==":70,"--":71,PCT:72,DEFAULT:73,SPACE:74,MINUS:75,keywords:76,UNICODE_TEXT:77,NUM:78,ALPHA:79,BQUOTE_STR:80,$accept:0,$end:1},terminals_:{2:"error",7:"direction_tb",8:"direction_bt",9:"direction_rl",10:"direction_lr",15:"NEWLINE",16:":",18:"open_directive",19:"type_directive",20:"arg_directive",21:"close_directive",22:"CLASS_DIAGRAM",24:"EOF",29:"GENERICTYPE",31:"LABEL",37:"CLASS",38:"STYLE_SEPARATOR",39:"STRUCT_START",41:"STRUCT_STOP",42:"ANNOTATION_START",43:"ANNOTATION_END",44:"MEMBER",45:"SEPARATOR",47:"STR",50:"AGGREGATION",51:"EXTENSION",52:"COMPOSITION",53:"DEPENDENCY",54:"LINE",55:"DOTTED_LINE",56:"CALLBACK",57:"LINK",58:"LINK_TARGET",59:"CLICK",60:"CALLBACK_NAME",61:"CALLBACK_ARGS",62:"HREF",63:"CSSCLASS",66:"graphCodeTokens",68:"TAGSTART",69:"TAGEND",70:"==",71:"--",72:"PCT",73:"DEFAULT",74:"SPACE",75:"MINUS",76:"keywords",77:"UNICODE_TEXT",78:"NUM",79:"ALPHA",80:"BQUOTE_STR"},productions_:[0,[3,1],[3,1],[3,2],[5,1],[5,1],[5,1],[5,1],[4,1],[6,4],[6,6],[12,1],[13,1],[17,1],[14,1],[11,4],[23,1],[23,2],[23,3],[26,1],[26,1],[26,2],[26,2],[26,2],[25,1],[25,2],[25,1],[25,1],[25,1],[25,1],[25,1],[25,1],[25,1],[32,2],[32,4],[32,5],[32,7],[34,4],[40,1],[40,2],[33,1],[33,2],[33,1],[33,1],[30,3],[30,4],[30,4],[30,5],[46,3],[46,2],[46,2],[46,1],[48,1],[48,1],[48,1],[48,1],[49,1],[49,1],[35,3],[35,4],[35,3],[35,4],[35,4],[35,5],[35,3],[35,4],[35,4],[35,5],[35,3],[35,4],[35,4],[35,5],[36,3],[64,1],[64,1],[65,1],[65,1],[65,1],[65,1],[65,1],[65,1],[65,1],[67,1],[67,1],[67,1],[67,1],[27,1],[27,1],[27,1],[28,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 4:r.setDirection("TB");break;case 5:r.setDirection("BT");break;case 6:r.setDirection("RL");break;case 7:r.setDirection("LR");break;case 11:r.parseDirective("%%{","open_directive");break;case 12:r.parseDirective(a[s],"type_directive");break;case 13:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 14:r.parseDirective("}%%","close_directive","class");break;case 19:case 20:this.$=a[s];break;case 21:this.$=a[s-1]+a[s];break;case 22:case 23:this.$=a[s-1]+"~"+a[s];break;case 24:r.addRelation(a[s]);break;case 25:a[s-1].title=r.cleanupLabel(a[s]),r.addRelation(a[s-1]);break;case 33:r.addClass(a[s]);break;case 34:r.addClass(a[s-2]),r.setCssClass(a[s-2],a[s]);break;case 35:r.addClass(a[s-3]),r.addMembers(a[s-3],a[s-1]);break;case 36:r.addClass(a[s-5]),r.setCssClass(a[s-5],a[s-3]),r.addMembers(a[s-5],a[s-1]);break;case 37:r.addAnnotation(a[s],a[s-2]);break;case 38:this.$=[a[s]];break;case 39:a[s].push(a[s-1]),this.$=a[s];break;case 40:case 42:case 43:break;case 41:r.addMember(a[s-1],r.cleanupLabel(a[s]));break;case 44:this.$={id1:a[s-2],id2:a[s],relation:a[s-1],relationTitle1:"none",relationTitle2:"none"};break;case 45:this.$={id1:a[s-3],id2:a[s],relation:a[s-1],relationTitle1:a[s-2],relationTitle2:"none"};break;case 46:this.$={id1:a[s-3],id2:a[s],relation:a[s-2],relationTitle1:"none",relationTitle2:a[s-1]};break;case 47:this.$={id1:a[s-4],id2:a[s],relation:a[s-2],relationTitle1:a[s-3],relationTitle2:a[s-1]};break;case 48:this.$={type1:a[s-2],type2:a[s],lineType:a[s-1]};break;case 49:this.$={type1:"none",type2:a[s],lineType:a[s-1]};break;case 50:this.$={type1:a[s-1],type2:"none",lineType:a[s]};break;case 51:this.$={type1:"none",type2:"none",lineType:a[s]};break;case 52:this.$=r.relationType.AGGREGATION;break;case 53:this.$=r.relationType.EXTENSION;break;case 54:this.$=r.relationType.COMPOSITION;break;case 55:this.$=r.relationType.DEPENDENCY;break;case 56:this.$=r.lineType.LINE;break;case 57:this.$=r.lineType.DOTTED_LINE;break;case 58:case 64:this.$=a[s-2],r.setClickEvent(a[s-1],a[s]);break;case 59:case 65:this.$=a[s-3],r.setClickEvent(a[s-2],a[s-1]),r.setTooltip(a[s-2],a[s]);break;case 60:case 68:this.$=a[s-2],r.setLink(a[s-1],a[s]);break;case 61:case 69:this.$=a[s-3],r.setLink(a[s-2],a[s-1],a[s]);break;case 62:case 70:this.$=a[s-3],r.setLink(a[s-2],a[s-1]),r.setTooltip(a[s-2],a[s]);break;case 63:case 71:this.$=a[s-4],r.setLink(a[s-3],a[s-2],a[s]),r.setTooltip(a[s-3],a[s-1]);break;case 66:this.$=a[s-3],r.setClickEvent(a[s-2],a[s-1],a[s]);break;case 67:this.$=a[s-4],r.setClickEvent(a[s-3],a[s-2],a[s-1]),r.setTooltip(a[s-3],a[s]);break;case 72:r.setCssClass(a[s-1],a[s])}},table:[{3:1,4:2,5:3,6:4,7:e,8:n,9:r,10:i,11:5,12:10,18:a,22:o},{1:[3]},{1:[2,1]},{1:[2,2]},{3:13,4:2,5:3,6:4,7:e,8:n,9:r,10:i,11:5,12:10,18:a,22:o},{1:[2,8]},t(s,[2,4]),t(s,[2,5]),t(s,[2,6]),t(s,[2,7]),{13:14,19:[1,15]},{15:[1,16]},{19:[2,11]},{1:[2,3]},{14:17,16:[1,18],21:c},t([16,21],[2,12]),{5:29,6:28,7:e,8:n,9:r,10:i,12:10,18:a,23:20,25:21,26:30,27:39,28:40,30:22,32:23,33:24,34:25,35:26,36:27,37:u,42:l,44:h,45:f,56:d,57:p,59:y,63:g,77:m,78:v,79:b,80:_},{15:[1,45]},{17:46,20:[1,47]},{15:[2,14]},{24:[1,48]},{15:[1,49],24:[2,16]},t(x,[2,24],{31:[1,50]}),t(x,[2,26]),t(x,[2,27]),t(x,[2,28]),t(x,[2,29]),t(x,[2,30]),t(x,[2,31]),t(x,[2,32]),t(x,[2,40],{46:51,48:54,49:55,31:[1,53],47:[1,52],50:w,51:k,52:T,53:E,54:C,55:S}),{26:62,27:39,28:40,77:m,78:v,79:b,80:_},t(x,[2,42]),t(x,[2,43]),{27:63,77:m,78:v,79:b},{26:64,27:39,28:40,77:m,78:v,79:b,80:_},{26:65,27:39,28:40,77:m,78:v,79:b,80:_},{26:66,27:39,28:40,77:m,78:v,79:b,80:_},{47:[1,67]},t(A,[2,19],{27:39,28:40,26:68,29:[1,69],77:m,78:v,79:b,80:_}),t(A,[2,20],{29:[1,70]}),t(M,[2,86]),t(M,[2,87]),t(M,[2,88]),t([15,24,29,31,38,39,47,50,51,52,53,54,55,60,62],[2,89]),t(N,[2,9]),{14:71,21:c},{21:[2,13]},{1:[2,15]},{5:29,6:28,7:e,8:n,9:r,10:i,12:10,18:a,23:72,24:[2,17],25:21,26:30,27:39,28:40,30:22,32:23,33:24,34:25,35:26,36:27,37:u,42:l,44:h,45:f,56:d,57:p,59:y,63:g,77:m,78:v,79:b,80:_},t(x,[2,25]),{26:73,27:39,28:40,47:[1,74],77:m,78:v,79:b,80:_},{46:75,48:54,49:55,50:w,51:k,52:T,53:E,54:C,55:S},t(x,[2,41]),{49:76,54:C,55:S},t(D,[2,51],{48:77,50:w,51:k,52:T,53:E}),t(O,[2,52]),t(O,[2,53]),t(O,[2,54]),t(O,[2,55]),t(B,[2,56]),t(B,[2,57]),t(x,[2,33],{38:[1,78],39:[1,79]}),{43:[1,80]},{47:[1,81]},{47:[1,82]},{60:[1,83],62:[1,84]},{27:85,77:m,78:v,79:b},t(A,[2,21]),t(A,[2,22]),t(A,[2,23]),{15:[1,86]},{24:[2,18]},t(L,[2,44]),{26:87,27:39,28:40,77:m,78:v,79:b,80:_},{26:88,27:39,28:40,47:[1,89],77:m,78:v,79:b,80:_},t(D,[2,50],{48:90,50:w,51:k,52:T,53:E}),t(D,[2,49]),{27:91,77:m,78:v,79:b},{40:92,44:I},{26:94,27:39,28:40,77:m,78:v,79:b,80:_},t(x,[2,58],{47:[1,95]}),t(x,[2,60],{47:[1,97],58:[1,96]}),t(x,[2,64],{47:[1,98],61:[1,99]}),t(x,[2,68],{47:[1,101],58:[1,100]}),t(x,[2,72]),t(N,[2,10]),t(L,[2,46]),t(L,[2,45]),{26:102,27:39,28:40,77:m,78:v,79:b,80:_},t(D,[2,48]),t(x,[2,34],{39:[1,103]}),{41:[1,104]},{40:105,41:[2,38],44:I},t(x,[2,37]),t(x,[2,59]),t(x,[2,61]),t(x,[2,62],{58:[1,106]}),t(x,[2,65]),t(x,[2,66],{47:[1,107]}),t(x,[2,69]),t(x,[2,70],{58:[1,108]}),t(L,[2,47]),{40:109,44:I},t(x,[2,35]),{41:[2,39]},t(x,[2,63]),t(x,[2,67]),t(x,[2,71]),{41:[1,110]},t(x,[2,36])],defaultActions:{2:[2,1],3:[2,2],5:[2,8],12:[2,11],13:[2,3],19:[2,14],47:[2,13],48:[2,15],72:[2,18],105:[2,39]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},F={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),18;case 1:return 7;case 2:return 8;case 3:return 9;case 4:return 10;case 5:return this.begin("type_directive"),19;case 6:return this.popState(),this.begin("arg_directive"),16;case 7:return this.popState(),this.popState(),21;case 8:return 20;case 9:case 10:case 12:case 19:break;case 11:return 15;case 13:case 14:return 22;case 15:return this.begin("struct"),39;case 16:return"EOF_IN_STRUCT";case 17:return"OPEN_IN_STRUCT";case 18:return this.popState(),41;case 20:return"MEMBER";case 21:return 37;case 22:return 63;case 23:return 56;case 24:return 57;case 25:return 59;case 26:return 42;case 27:return 43;case 28:this.begin("generic");break;case 29:case 32:case 35:case 38:case 41:case 44:this.popState();break;case 30:return"GENERICTYPE";case 31:this.begin("string");break;case 33:return"STR";case 34:this.begin("bqstring");break;case 36:return"BQUOTE_STR";case 37:this.begin("href");break;case 39:return 62;case 40:this.begin("callback_name");break;case 42:this.popState(),this.begin("callback_args");break;case 43:return 60;case 45:return 61;case 46:case 47:case 48:case 49:return 58;case 50:case 51:return 51;case 52:case 53:return 53;case 54:return 52;case 55:return 50;case 56:return 54;case 57:return 55;case 58:return 31;case 59:return 38;case 60:return 75;case 61:return"DOT";case 62:return"PLUS";case 63:return 72;case 64:case 65:return"EQUALS";case 66:return 79;case 67:return"PUNCTUATION";case 68:return 78;case 69:return 77;case 70:return 74;case 71:return 24}},rules:[/^(?:%%\{)/,/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:((?:(?!\}%%)[^:.])*))/,/^(?::)/,/^(?:\}%%)/,/^(?:((?:(?!\}%%).|\n)*))/,/^(?:%%(?!\{)*[^\n]*(\r?\n?)+)/,/^(?:%%[^\n]*(\r?\n)*)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:classDiagram-v2\b)/,/^(?:classDiagram\b)/,/^(?:[{])/,/^(?:$)/,/^(?:[{])/,/^(?:[}])/,/^(?:[\n])/,/^(?:[^{}\n]*)/,/^(?:class\b)/,/^(?:cssClass\b)/,/^(?:callback\b)/,/^(?:link\b)/,/^(?:click\b)/,/^(?:<<)/,/^(?:>>)/,/^(?:[~])/,/^(?:[~])/,/^(?:[^~]*)/,/^(?:["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:[`])/,/^(?:[`])/,/^(?:[^`]+)/,/^(?:href[\s]+["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:\s*<\|)/,/^(?:\s*\|>)/,/^(?:\s*>)/,/^(?:\s*<)/,/^(?:\s*\*)/,/^(?:\s*o\b)/,/^(?:--)/,/^(?:\.\.)/,/^(?::{1}[^:\n;]+)/,/^(?::{3})/,/^(?:-)/,/^(?:\.)/,/^(?:\+)/,/^(?:%)/,/^(?:=)/,/^(?:=)/,/^(?:\w+)/,/^(?:[!"#$%&'*+,-.`?\\/])/,/^(?:[0-9]+)/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|[\uFFD2-\uFFD7\uFFDA-\uFFDC])/,/^(?:\s)/,/^(?:$)/],conditions:{arg_directive:{rules:[7,8],inclusive:!1},type_directive:{rules:[6,7],inclusive:!1},open_directive:{rules:[5],inclusive:!1},callback_args:{rules:[44,45],inclusive:!1},callback_name:{rules:[41,42,43],inclusive:!1},href:{rules:[38,39],inclusive:!1},struct:{rules:[16,17,18,19,20],inclusive:!1},generic:{rules:[29,30],inclusive:!1},bqstring:{rules:[35,36],inclusive:!1},string:{rules:[32,33],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,9,10,11,12,13,14,15,21,22,23,24,25,26,27,28,31,34,37,40,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71],inclusive:!0}}};function P(){this.yy={}}return R.lexer=F,P.prototype=R,R.Parser=P,new P}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(8218).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},5890:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,2],n=[1,5],r=[6,9,11,23,41],i=[1,17],a=[1,20],o=[1,25],s=[1,26],c=[1,27],u=[1,28],l=[1,37],h=[23,38,39],f=[4,6,9,11,23,41],d=[34,35,36,37],p=[22,29],y=[1,55],g={trace:function(){},yy:{},symbols_:{error:2,start:3,ER_DIAGRAM:4,document:5,EOF:6,directive:7,line:8,SPACE:9,statement:10,NEWLINE:11,openDirective:12,typeDirective:13,closeDirective:14,":":15,argDirective:16,entityName:17,relSpec:18,role:19,BLOCK_START:20,attributes:21,BLOCK_STOP:22,ALPHANUM:23,attribute:24,attributeType:25,attributeName:26,attributeKeyType:27,attributeComment:28,ATTRIBUTE_WORD:29,ATTRIBUTE_KEY:30,COMMENT:31,cardinality:32,relType:33,ZERO_OR_ONE:34,ZERO_OR_MORE:35,ONE_OR_MORE:36,ONLY_ONE:37,NON_IDENTIFYING:38,IDENTIFYING:39,WORD:40,open_directive:41,type_directive:42,arg_directive:43,close_directive:44,$accept:0,$end:1},terminals_:{2:"error",4:"ER_DIAGRAM",6:"EOF",9:"SPACE",11:"NEWLINE",15:":",20:"BLOCK_START",22:"BLOCK_STOP",23:"ALPHANUM",29:"ATTRIBUTE_WORD",30:"ATTRIBUTE_KEY",31:"COMMENT",34:"ZERO_OR_ONE",35:"ZERO_OR_MORE",36:"ONE_OR_MORE",37:"ONLY_ONE",38:"NON_IDENTIFYING",39:"IDENTIFYING",40:"WORD",41:"open_directive",42:"type_directive",43:"arg_directive",44:"close_directive"},productions_:[0,[3,3],[3,2],[5,0],[5,2],[8,2],[8,1],[8,1],[8,1],[7,4],[7,6],[10,1],[10,5],[10,4],[10,3],[10,1],[17,1],[21,1],[21,2],[24,2],[24,3],[24,3],[24,4],[25,1],[26,1],[27,1],[28,1],[18,3],[32,1],[32,1],[32,1],[32,1],[33,1],[33,1],[19,1],[19,1],[12,1],[13,1],[16,1],[14,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 1:break;case 3:case 7:case 8:this.$=[];break;case 4:a[s-1].push(a[s]),this.$=a[s-1];break;case 5:case 6:case 16:case 23:case 24:case 25:case 35:this.$=a[s];break;case 12:r.addEntity(a[s-4]),r.addEntity(a[s-2]),r.addRelationship(a[s-4],a[s],a[s-2],a[s-3]);break;case 13:r.addEntity(a[s-3]),r.addAttributes(a[s-3],a[s-1]);break;case 14:r.addEntity(a[s-2]);break;case 15:r.addEntity(a[s]);break;case 17:this.$=[a[s]];break;case 18:a[s].push(a[s-1]),this.$=a[s];break;case 19:this.$={attributeType:a[s-1],attributeName:a[s]};break;case 20:this.$={attributeType:a[s-2],attributeName:a[s-1],attributeKeyType:a[s]};break;case 21:this.$={attributeType:a[s-2],attributeName:a[s-1],attributeComment:a[s]};break;case 22:this.$={attributeType:a[s-3],attributeName:a[s-2],attributeKeyType:a[s-1],attributeComment:a[s]};break;case 26:case 34:this.$=a[s].replace(/"/g,"");break;case 27:this.$={cardA:a[s],relType:a[s-1],cardB:a[s-2]};break;case 28:this.$=r.Cardinality.ZERO_OR_ONE;break;case 29:this.$=r.Cardinality.ZERO_OR_MORE;break;case 30:this.$=r.Cardinality.ONE_OR_MORE;break;case 31:this.$=r.Cardinality.ONLY_ONE;break;case 32:this.$=r.Identification.NON_IDENTIFYING;break;case 33:this.$=r.Identification.IDENTIFYING;break;case 36:r.parseDirective("%%{","open_directive");break;case 37:r.parseDirective(a[s],"type_directive");break;case 38:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 39:r.parseDirective("}%%","close_directive","er")}},table:[{3:1,4:e,7:3,12:4,41:n},{1:[3]},t(r,[2,3],{5:6}),{3:7,4:e,7:3,12:4,41:n},{13:8,42:[1,9]},{42:[2,36]},{6:[1,10],7:15,8:11,9:[1,12],10:13,11:[1,14],12:4,17:16,23:i,41:n},{1:[2,2]},{14:18,15:[1,19],44:a},t([15,44],[2,37]),t(r,[2,8],{1:[2,1]}),t(r,[2,4]),{7:15,10:21,12:4,17:16,23:i,41:n},t(r,[2,6]),t(r,[2,7]),t(r,[2,11]),t(r,[2,15],{18:22,32:24,20:[1,23],34:o,35:s,36:c,37:u}),t([6,9,11,15,20,23,34,35,36,37,41],[2,16]),{11:[1,29]},{16:30,43:[1,31]},{11:[2,39]},t(r,[2,5]),{17:32,23:i},{21:33,22:[1,34],24:35,25:36,29:l},{33:38,38:[1,39],39:[1,40]},t(h,[2,28]),t(h,[2,29]),t(h,[2,30]),t(h,[2,31]),t(f,[2,9]),{14:41,44:a},{44:[2,38]},{15:[1,42]},{22:[1,43]},t(r,[2,14]),{21:44,22:[2,17],24:35,25:36,29:l},{26:45,29:[1,46]},{29:[2,23]},{32:47,34:o,35:s,36:c,37:u},t(d,[2,32]),t(d,[2,33]),{11:[1,48]},{19:49,23:[1,51],40:[1,50]},t(r,[2,13]),{22:[2,18]},t(p,[2,19],{27:52,28:53,30:[1,54],31:y}),t([22,29,30,31],[2,24]),{23:[2,27]},t(f,[2,10]),t(r,[2,12]),t(r,[2,34]),t(r,[2,35]),t(p,[2,20],{28:56,31:y}),t(p,[2,21]),t([22,29,31],[2,25]),t(p,[2,26]),t(p,[2,22])],defaultActions:{5:[2,36],7:[2,2],20:[2,39],31:[2,38],37:[2,23],44:[2,18],47:[2,27]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},m={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),41;case 1:return this.begin("type_directive"),42;case 2:return this.popState(),this.begin("arg_directive"),15;case 3:return this.popState(),this.popState(),44;case 4:return 43;case 5:case 6:case 8:case 13:case 17:break;case 7:return 11;case 9:return 9;case 10:return 40;case 11:return 4;case 12:return this.begin("block"),20;case 14:return 30;case 15:return 29;case 16:return 31;case 18:return this.popState(),22;case 19:case 32:return e.yytext[0];case 20:case 24:return 34;case 21:case 25:return 35;case 22:case 26:return 36;case 23:return 37;case 27:case 29:case 30:return 38;case 28:return 39;case 31:return 23;case 33:return 6}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:[\s]+)/i,/^(?:"[^"]*")/i,/^(?:erDiagram\b)/i,/^(?:\{)/i,/^(?:\s+)/i,/^(?:(?:PK)|(?:FK))/i,/^(?:[A-Za-z][A-Za-z0-9\-_]*)/i,/^(?:"[^"]*")/i,/^(?:[\n]+)/i,/^(?:\})/i,/^(?:.)/i,/^(?:\|o\b)/i,/^(?:\}o\b)/i,/^(?:\}\|)/i,/^(?:\|\|)/i,/^(?:o\|)/i,/^(?:o\{)/i,/^(?:\|\{)/i,/^(?:\.\.)/i,/^(?:--)/i,/^(?:\.-)/i,/^(?:-\.)/i,/^(?:[A-Za-z][A-Za-z0-9\-_]*)/i,/^(?:.)/i,/^(?:$)/i],conditions:{open_directive:{rules:[1],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},block:{rules:[13,14,15,16,17,18,19],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,11,12,20,21,22,23,24,25,26,27,28,29,30,31,32,33],inclusive:!0}}};function v(){this.yy={}}return g.lexer=m,v.prototype=g,g.Parser=v,new v}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(8009).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},3602:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,9],n=[1,7],r=[1,6],i=[1,8],a=[1,20,21,22,23,38,47,59,60,79,80,81,82,83,84,88,98,99,102,104,105,111,112,113,114,115,116,117,118,119,120],o=[2,10],s=[1,20],c=[1,21],u=[1,22],l=[1,23],h=[1,30],f=[1,59],d=[1,45],p=[1,49],y=[1,33],g=[1,34],m=[1,35],v=[1,36],b=[1,37],_=[1,53],x=[1,60],w=[1,48],k=[1,50],T=[1,52],E=[1,56],C=[1,57],S=[1,38],A=[1,39],M=[1,40],N=[1,41],D=[1,58],O=[1,47],B=[1,51],L=[1,54],I=[1,55],R=[1,46],F=[1,63],P=[1,68],j=[1,20,21,22,23,38,42,47,59,60,79,80,81,82,83,84,88,98,99,102,104,105,111,112,113,114,115,116,117,118,119,120],Y=[1,72],z=[1,71],U=[1,73],q=[20,21,23,74,75],H=[1,94],$=[1,99],W=[1,102],V=[1,103],G=[1,96],X=[1,101],Z=[1,104],Q=[1,97],K=[1,109],J=[1,108],tt=[1,98],et=[1,100],nt=[1,105],rt=[1,106],it=[1,107],at=[1,110],ot=[20,21,22,23,74,75],st=[20,21,22,23,48,74,75],ct=[20,21,22,23,40,47,48,50,52,54,56,58,59,60,62,64,66,67,69,74,75,84,88,98,99,102,104,105,115,116,117,118,119,120],ut=[20,21,23],lt=[20,21,23,47,59,60,74,75,84,88,98,99,102,104,105,115,116,117,118,119,120],ht=[1,12,20,21,22,23,24,38,42,47,59,60,79,80,81,82,83,84,88,98,99,102,104,105,111,112,113,114,115,116,117,118,119,120],ft=[47,59,60,84,88,98,99,102,104,105,115,116,117,118,119,120],dt=[1,143],pt=[1,151],yt=[1,152],gt=[1,153],mt=[1,154],vt=[1,138],bt=[1,139],_t=[1,135],xt=[1,146],wt=[1,147],kt=[1,148],Tt=[1,149],Et=[1,150],Ct=[1,155],St=[1,156],At=[1,141],Mt=[1,144],Nt=[1,140],Dt=[1,137],Ot=[20,21,22,23,38,42,47,59,60,79,80,81,82,83,84,88,98,99,102,104,105,111,112,113,114,115,116,117,118,119,120],Bt=[1,159],Lt=[20,21,22,23,26,47,59,60,84,98,99,102,104,105,115,116,117,118,119,120],It=[20,21,22,23,24,26,38,40,41,42,47,51,53,55,57,59,60,61,63,65,66,68,70,74,75,79,80,81,82,83,84,85,88,98,99,102,104,105,106,107,115,116,117,118,119,120],Rt=[12,21,22,24],Ft=[22,99],Pt=[1,242],jt=[1,237],Yt=[1,238],zt=[1,246],Ut=[1,243],qt=[1,240],Ht=[1,239],$t=[1,241],Wt=[1,244],Vt=[1,245],Gt=[1,247],Xt=[1,265],Zt=[20,21,23,99],Qt=[20,21,22,23,59,60,79,95,98,99,102,103,104,105,106],Kt={trace:function(){},yy:{},symbols_:{error:2,start:3,mermaidDoc:4,directive:5,openDirective:6,typeDirective:7,closeDirective:8,separator:9,":":10,argDirective:11,open_directive:12,type_directive:13,arg_directive:14,close_directive:15,graphConfig:16,document:17,line:18,statement:19,SEMI:20,NEWLINE:21,SPACE:22,EOF:23,GRAPH:24,NODIR:25,DIR:26,FirstStmtSeperator:27,ending:28,endToken:29,spaceList:30,spaceListNewline:31,verticeStatement:32,styleStatement:33,linkStyleStatement:34,classDefStatement:35,classStatement:36,clickStatement:37,subgraph:38,text:39,SQS:40,SQE:41,end:42,direction:43,link:44,node:45,vertex:46,AMP:47,STYLE_SEPARATOR:48,idString:49,PS:50,PE:51,"(-":52,"-)":53,STADIUMSTART:54,STADIUMEND:55,SUBROUTINESTART:56,SUBROUTINEEND:57,VERTEX_WITH_PROPS_START:58,ALPHA:59,COLON:60,PIPE:61,CYLINDERSTART:62,CYLINDEREND:63,DIAMOND_START:64,DIAMOND_STOP:65,TAGEND:66,TRAPSTART:67,TRAPEND:68,INVTRAPSTART:69,INVTRAPEND:70,linkStatement:71,arrowText:72,TESTSTR:73,START_LINK:74,LINK:75,textToken:76,STR:77,keywords:78,STYLE:79,LINKSTYLE:80,CLASSDEF:81,CLASS:82,CLICK:83,DOWN:84,UP:85,textNoTags:86,textNoTagsToken:87,DEFAULT:88,stylesOpt:89,alphaNum:90,CALLBACKNAME:91,CALLBACKARGS:92,HREF:93,LINK_TARGET:94,HEX:95,numList:96,INTERPOLATE:97,NUM:98,COMMA:99,style:100,styleComponent:101,MINUS:102,UNIT:103,BRKT:104,DOT:105,PCT:106,TAGSTART:107,alphaNumToken:108,idStringToken:109,alphaNumStatement:110,direction_tb:111,direction_bt:112,direction_rl:113,direction_lr:114,PUNCTUATION:115,UNICODE_TEXT:116,PLUS:117,EQUALS:118,MULT:119,UNDERSCORE:120,graphCodeTokens:121,ARROW_CROSS:122,ARROW_POINT:123,ARROW_CIRCLE:124,ARROW_OPEN:125,QUOTE:126,$accept:0,$end:1},terminals_:{2:"error",10:":",12:"open_directive",13:"type_directive",14:"arg_directive",15:"close_directive",20:"SEMI",21:"NEWLINE",22:"SPACE",23:"EOF",24:"GRAPH",25:"NODIR",26:"DIR",38:"subgraph",40:"SQS",41:"SQE",42:"end",47:"AMP",48:"STYLE_SEPARATOR",50:"PS",51:"PE",52:"(-",53:"-)",54:"STADIUMSTART",55:"STADIUMEND",56:"SUBROUTINESTART",57:"SUBROUTINEEND",58:"VERTEX_WITH_PROPS_START",59:"ALPHA",60:"COLON",61:"PIPE",62:"CYLINDERSTART",63:"CYLINDEREND",64:"DIAMOND_START",65:"DIAMOND_STOP",66:"TAGEND",67:"TRAPSTART",68:"TRAPEND",69:"INVTRAPSTART",70:"INVTRAPEND",73:"TESTSTR",74:"START_LINK",75:"LINK",77:"STR",79:"STYLE",80:"LINKSTYLE",81:"CLASSDEF",82:"CLASS",83:"CLICK",84:"DOWN",85:"UP",88:"DEFAULT",91:"CALLBACKNAME",92:"CALLBACKARGS",93:"HREF",94:"LINK_TARGET",95:"HEX",97:"INTERPOLATE",98:"NUM",99:"COMMA",102:"MINUS",103:"UNIT",104:"BRKT",105:"DOT",106:"PCT",107:"TAGSTART",111:"direction_tb",112:"direction_bt",113:"direction_rl",114:"direction_lr",115:"PUNCTUATION",116:"UNICODE_TEXT",117:"PLUS",118:"EQUALS",119:"MULT",120:"UNDERSCORE",122:"ARROW_CROSS",123:"ARROW_POINT",124:"ARROW_CIRCLE",125:"ARROW_OPEN",126:"QUOTE"},productions_:[0,[3,1],[3,2],[5,4],[5,6],[6,1],[7,1],[11,1],[8,1],[4,2],[17,0],[17,2],[18,1],[18,1],[18,1],[18,1],[18,1],[16,2],[16,2],[16,2],[16,3],[28,2],[28,1],[29,1],[29,1],[29,1],[27,1],[27,1],[27,2],[31,2],[31,2],[31,1],[31,1],[30,2],[30,1],[19,2],[19,2],[19,2],[19,2],[19,2],[19,2],[19,9],[19,6],[19,4],[19,1],[9,1],[9,1],[9,1],[32,3],[32,4],[32,2],[32,1],[45,1],[45,5],[45,3],[46,4],[46,6],[46,4],[46,4],[46,4],[46,8],[46,4],[46,4],[46,4],[46,6],[46,4],[46,4],[46,4],[46,4],[46,4],[46,1],[44,2],[44,3],[44,3],[44,1],[44,3],[71,1],[72,3],[39,1],[39,2],[39,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[78,1],[86,1],[86,2],[35,5],[35,5],[36,5],[37,2],[37,4],[37,3],[37,5],[37,2],[37,4],[37,4],[37,6],[37,2],[37,4],[37,2],[37,4],[37,4],[37,6],[33,5],[33,5],[34,5],[34,5],[34,9],[34,9],[34,7],[34,7],[96,1],[96,3],[89,1],[89,3],[100,1],[100,2],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[101,1],[76,1],[76,1],[76,1],[76,1],[76,1],[76,1],[87,1],[87,1],[87,1],[87,1],[49,1],[49,2],[90,1],[90,2],[110,1],[110,1],[110,1],[110,1],[43,1],[43,1],[43,1],[43,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[109,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1],[121,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 5:r.parseDirective("%%{","open_directive");break;case 6:r.parseDirective(a[s],"type_directive");break;case 7:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 8:r.parseDirective("}%%","close_directive","flowchart");break;case 10:case 36:case 37:case 38:case 39:case 40:this.$=[];break;case 11:a[s]!==[]&&a[s-1].push(a[s]),this.$=a[s-1];break;case 12:case 78:case 80:case 92:case 148:case 150:case 151:case 74:case 146:this.$=a[s];break;case 19:r.setDirection("TB"),this.$="TB";break;case 20:r.setDirection(a[s-1]),this.$=a[s-1];break;case 35:this.$=a[s-1].nodes;break;case 41:this.$=r.addSubGraph(a[s-6],a[s-1],a[s-4]);break;case 42:this.$=r.addSubGraph(a[s-3],a[s-1],a[s-3]);break;case 43:this.$=r.addSubGraph(void 0,a[s-1],void 0);break;case 48:r.addLink(a[s-2].stmt,a[s],a[s-1]),this.$={stmt:a[s],nodes:a[s].concat(a[s-2].nodes)};break;case 49:r.addLink(a[s-3].stmt,a[s-1],a[s-2]),this.$={stmt:a[s-1],nodes:a[s-1].concat(a[s-3].nodes)};break;case 50:this.$={stmt:a[s-1],nodes:a[s-1]};break;case 51:this.$={stmt:a[s],nodes:a[s]};break;case 52:case 119:case 121:this.$=[a[s]];break;case 53:this.$=a[s-4].concat(a[s]);break;case 54:this.$=[a[s-2]],r.setClass(a[s-2],a[s]);break;case 55:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"square");break;case 56:this.$=a[s-5],r.addVertex(a[s-5],a[s-2],"circle");break;case 57:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"ellipse");break;case 58:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"stadium");break;case 59:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"subroutine");break;case 60:this.$=a[s-7],r.addVertex(a[s-7],a[s-1],"rect",void 0,void 0,void 0,Object.fromEntries([[a[s-5],a[s-3]]]));break;case 61:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"cylinder");break;case 62:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"round");break;case 63:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"diamond");break;case 64:this.$=a[s-5],r.addVertex(a[s-5],a[s-2],"hexagon");break;case 65:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"odd");break;case 66:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"trapezoid");break;case 67:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"inv_trapezoid");break;case 68:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"lean_right");break;case 69:this.$=a[s-3],r.addVertex(a[s-3],a[s-1],"lean_left");break;case 70:this.$=a[s],r.addVertex(a[s]);break;case 71:a[s-1].text=a[s],this.$=a[s-1];break;case 72:case 73:a[s-2].text=a[s-1],this.$=a[s-2];break;case 75:var c=r.destructLink(a[s],a[s-2]);this.$={type:c.type,stroke:c.stroke,length:c.length,text:a[s-1]};break;case 76:c=r.destructLink(a[s]),this.$={type:c.type,stroke:c.stroke,length:c.length};break;case 77:this.$=a[s-1];break;case 79:case 93:case 149:case 147:this.$=a[s-1]+""+a[s];break;case 94:case 95:this.$=a[s-4],r.addClass(a[s-2],a[s]);break;case 96:this.$=a[s-4],r.setClass(a[s-2],a[s]);break;case 97:case 105:this.$=a[s-1],r.setClickEvent(a[s-1],a[s]);break;case 98:case 106:this.$=a[s-3],r.setClickEvent(a[s-3],a[s-2]),r.setTooltip(a[s-3],a[s]);break;case 99:this.$=a[s-2],r.setClickEvent(a[s-2],a[s-1],a[s]);break;case 100:this.$=a[s-4],r.setClickEvent(a[s-4],a[s-3],a[s-2]),r.setTooltip(a[s-4],a[s]);break;case 101:case 107:this.$=a[s-1],r.setLink(a[s-1],a[s]);break;case 102:case 108:this.$=a[s-3],r.setLink(a[s-3],a[s-2]),r.setTooltip(a[s-3],a[s]);break;case 103:case 109:this.$=a[s-3],r.setLink(a[s-3],a[s-2],a[s]);break;case 104:case 110:this.$=a[s-5],r.setLink(a[s-5],a[s-4],a[s]),r.setTooltip(a[s-5],a[s-2]);break;case 111:this.$=a[s-4],r.addVertex(a[s-2],void 0,void 0,a[s]);break;case 112:case 114:this.$=a[s-4],r.updateLink(a[s-2],a[s]);break;case 113:this.$=a[s-4],r.updateLink([a[s-2]],a[s]);break;case 115:this.$=a[s-8],r.updateLinkInterpolate([a[s-6]],a[s-2]),r.updateLink([a[s-6]],a[s]);break;case 116:this.$=a[s-8],r.updateLinkInterpolate(a[s-6],a[s-2]),r.updateLink(a[s-6],a[s]);break;case 117:this.$=a[s-6],r.updateLinkInterpolate([a[s-4]],a[s]);break;case 118:this.$=a[s-6],r.updateLinkInterpolate(a[s-4],a[s]);break;case 120:case 122:a[s-2].push(a[s]),this.$=a[s-2];break;case 124:this.$=a[s-1]+a[s];break;case 152:this.$="v";break;case 153:this.$="-";break;case 154:this.$={stmt:"dir",value:"TB"};break;case 155:this.$={stmt:"dir",value:"BT"};break;case 156:this.$={stmt:"dir",value:"RL"};break;case 157:this.$={stmt:"dir",value:"LR"}}},table:[{3:1,4:2,5:3,6:5,12:e,16:4,21:n,22:r,24:i},{1:[3]},{1:[2,1]},{3:10,4:2,5:3,6:5,12:e,16:4,21:n,22:r,24:i},t(a,o,{17:11}),{7:12,13:[1,13]},{16:14,21:n,22:r,24:i},{16:15,21:n,22:r,24:i},{25:[1,16],26:[1,17]},{13:[2,5]},{1:[2,2]},{1:[2,9],18:18,19:19,20:s,21:c,22:u,23:l,32:24,33:25,34:26,35:27,36:28,37:29,38:h,43:31,45:32,46:42,47:f,49:43,59:d,60:p,79:y,80:g,81:m,82:v,83:b,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,111:S,112:A,113:M,114:N,115:D,116:O,117:B,118:L,119:I,120:R},{8:61,10:[1,62],15:F},t([10,15],[2,6]),t(a,[2,17]),t(a,[2,18]),t(a,[2,19]),{20:[1,65],21:[1,66],22:P,27:64,30:67},t(j,[2,11]),t(j,[2,12]),t(j,[2,13]),t(j,[2,14]),t(j,[2,15]),t(j,[2,16]),{9:69,20:Y,21:z,23:U,44:70,71:74,74:[1,75],75:[1,76]},{9:77,20:Y,21:z,23:U},{9:78,20:Y,21:z,23:U},{9:79,20:Y,21:z,23:U},{9:80,20:Y,21:z,23:U},{9:81,20:Y,21:z,23:U},{9:83,20:Y,21:z,22:[1,82],23:U},t(j,[2,44]),t(q,[2,51],{30:84,22:P}),{22:[1,85]},{22:[1,86]},{22:[1,87]},{22:[1,88]},{26:H,47:$,59:W,60:V,77:[1,92],84:G,90:91,91:[1,89],93:[1,90],98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(j,[2,154]),t(j,[2,155]),t(j,[2,156]),t(j,[2,157]),t(ot,[2,52],{48:[1,111]}),t(st,[2,70],{109:123,40:[1,112],47:f,50:[1,113],52:[1,114],54:[1,115],56:[1,116],58:[1,117],59:d,60:p,62:[1,118],64:[1,119],66:[1,120],67:[1,121],69:[1,122],84:_,88:x,98:w,99:k,102:T,104:E,105:C,115:D,116:O,117:B,118:L,119:I,120:R}),t(ct,[2,146]),t(ct,[2,171]),t(ct,[2,172]),t(ct,[2,173]),t(ct,[2,174]),t(ct,[2,175]),t(ct,[2,176]),t(ct,[2,177]),t(ct,[2,178]),t(ct,[2,179]),t(ct,[2,180]),t(ct,[2,181]),t(ct,[2,182]),t(ct,[2,183]),t(ct,[2,184]),t(ct,[2,185]),t(ct,[2,186]),{9:124,20:Y,21:z,23:U},{11:125,14:[1,126]},t(ut,[2,8]),t(a,[2,20]),t(a,[2,26]),t(a,[2,27]),{21:[1,127]},t(lt,[2,34],{30:128,22:P}),t(j,[2,35]),{45:129,46:42,47:f,49:43,59:d,60:p,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,115:D,116:O,117:B,118:L,119:I,120:R},t(ht,[2,45]),t(ht,[2,46]),t(ht,[2,47]),t(ft,[2,74],{72:130,61:[1,132],73:[1,131]}),{22:dt,24:pt,26:yt,38:gt,39:133,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t([47,59,60,61,73,84,88,98,99,102,104,105,115,116,117,118,119,120],[2,76]),t(j,[2,36]),t(j,[2,37]),t(j,[2,38]),t(j,[2,39]),t(j,[2,40]),{22:dt,24:pt,26:yt,38:gt,39:157,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(Ot,o,{17:158}),t(q,[2,50],{47:Bt}),{26:H,47:$,59:W,60:V,84:G,90:160,95:[1,161],98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},{88:[1,162],96:163,98:[1,164]},{26:H,47:$,59:W,60:V,84:G,88:[1,165],90:166,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},{26:H,47:$,59:W,60:V,84:G,90:167,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ut,[2,97],{22:[1,168],92:[1,169]}),t(ut,[2,101],{22:[1,170]}),t(ut,[2,105],{108:95,110:172,22:[1,171],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,115:tt,116:et,117:nt,118:rt,119:it,120:at}),t(ut,[2,107],{22:[1,173]}),t(Lt,[2,148]),t(Lt,[2,150]),t(Lt,[2,151]),t(Lt,[2,152]),t(Lt,[2,153]),t(It,[2,158]),t(It,[2,159]),t(It,[2,160]),t(It,[2,161]),t(It,[2,162]),t(It,[2,163]),t(It,[2,164]),t(It,[2,165]),t(It,[2,166]),t(It,[2,167]),t(It,[2,168]),t(It,[2,169]),t(It,[2,170]),{47:f,49:174,59:d,60:p,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,115:D,116:O,117:B,118:L,119:I,120:R},{22:dt,24:pt,26:yt,38:gt,39:175,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:177,42:mt,47:$,50:[1,176],59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:178,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:179,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:180,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{59:[1,181]},{22:dt,24:pt,26:yt,38:gt,39:182,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:183,42:mt,47:$,59:W,60:V,64:[1,184],66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:185,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:186,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:187,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ct,[2,147]),t(Rt,[2,3]),{8:188,15:F},{15:[2,7]},t(a,[2,28]),t(lt,[2,33]),t(q,[2,48],{30:189,22:P}),t(ft,[2,71],{22:[1,190]}),{22:[1,191]},{22:dt,24:pt,26:yt,38:gt,39:192,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,66:vt,74:bt,75:[1,193],76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(It,[2,78]),t(It,[2,80]),t(It,[2,136]),t(It,[2,137]),t(It,[2,138]),t(It,[2,139]),t(It,[2,140]),t(It,[2,141]),t(It,[2,142]),t(It,[2,143]),t(It,[2,144]),t(It,[2,145]),t(It,[2,81]),t(It,[2,82]),t(It,[2,83]),t(It,[2,84]),t(It,[2,85]),t(It,[2,86]),t(It,[2,87]),t(It,[2,88]),t(It,[2,89]),t(It,[2,90]),t(It,[2,91]),{9:196,20:Y,21:z,22:dt,23:U,24:pt,26:yt,38:gt,40:[1,195],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{18:18,19:19,20:s,21:c,22:u,23:l,32:24,33:25,34:26,35:27,36:28,37:29,38:h,42:[1,197],43:31,45:32,46:42,47:f,49:43,59:d,60:p,79:y,80:g,81:m,82:v,83:b,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,111:S,112:A,113:M,114:N,115:D,116:O,117:B,118:L,119:I,120:R},{22:P,30:198},{22:[1,199],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,108:95,110:172,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:[1,200]},{22:[1,201]},{22:[1,202],99:[1,203]},t(Ft,[2,119]),{22:[1,204]},{22:[1,205],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,108:95,110:172,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:[1,206],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,108:95,110:172,115:tt,116:et,117:nt,118:rt,119:it,120:at},{77:[1,207]},t(ut,[2,99],{22:[1,208]}),{77:[1,209],94:[1,210]},{77:[1,211]},t(Lt,[2,149]),{77:[1,212],94:[1,213]},t(ot,[2,54],{109:123,47:f,59:d,60:p,84:_,88:x,98:w,99:k,102:T,104:E,105:C,115:D,116:O,117:B,118:L,119:I,120:R}),{22:dt,24:pt,26:yt,38:gt,41:[1,214],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:215,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,51:[1,216],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,53:[1,217],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,55:[1,218],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,57:[1,219],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{60:[1,220]},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,63:[1,221],66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,65:[1,222],66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,39:223,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,41:[1,224],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,66:vt,68:[1,225],70:[1,226],74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,66:vt,68:[1,228],70:[1,227],74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{9:229,20:Y,21:z,23:U},t(q,[2,49],{47:Bt}),t(ft,[2,73]),t(ft,[2,72]),{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,61:[1,230],66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ft,[2,75]),t(It,[2,79]),{22:dt,24:pt,26:yt,38:gt,39:231,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(Ot,o,{17:232}),t(j,[2,43]),{46:233,47:f,49:43,59:d,60:p,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,115:D,116:O,117:B,118:L,119:I,120:R},{22:Pt,59:jt,60:Yt,79:zt,89:234,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:248,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:249,95:Ut,97:[1,250],98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:251,95:Ut,97:[1,252],98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{98:[1,253]},{22:Pt,59:jt,60:Yt,79:zt,89:254,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:255,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{26:H,47:$,59:W,60:V,84:G,90:256,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ut,[2,98]),{77:[1,257]},t(ut,[2,102],{22:[1,258]}),t(ut,[2,103]),t(ut,[2,106]),t(ut,[2,108],{22:[1,259]}),t(ut,[2,109]),t(st,[2,55]),{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,51:[1,260],59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(st,[2,62]),t(st,[2,57]),t(st,[2,58]),t(st,[2,59]),{59:[1,261]},t(st,[2,61]),t(st,[2,63]),{22:dt,24:pt,26:yt,38:gt,42:mt,47:$,59:W,60:V,65:[1,262],66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(st,[2,65]),t(st,[2,66]),t(st,[2,68]),t(st,[2,67]),t(st,[2,69]),t(Rt,[2,4]),t([22,47,59,60,84,88,98,99,102,104,105,115,116,117,118,119,120],[2,77]),{22:dt,24:pt,26:yt,38:gt,41:[1,263],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{18:18,19:19,20:s,21:c,22:u,23:l,32:24,33:25,34:26,35:27,36:28,37:29,38:h,42:[1,264],43:31,45:32,46:42,47:f,49:43,59:d,60:p,79:y,80:g,81:m,82:v,83:b,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,111:S,112:A,113:M,114:N,115:D,116:O,117:B,118:L,119:I,120:R},t(ot,[2,53]),t(ut,[2,111],{99:Xt}),t(Zt,[2,121],{101:266,22:Pt,59:jt,60:Yt,79:zt,95:Ut,98:qt,102:Ht,103:$t,104:Wt,105:Vt,106:Gt}),t(Qt,[2,123]),t(Qt,[2,125]),t(Qt,[2,126]),t(Qt,[2,127]),t(Qt,[2,128]),t(Qt,[2,129]),t(Qt,[2,130]),t(Qt,[2,131]),t(Qt,[2,132]),t(Qt,[2,133]),t(Qt,[2,134]),t(Qt,[2,135]),t(ut,[2,112],{99:Xt}),t(ut,[2,113],{99:Xt}),{22:[1,267]},t(ut,[2,114],{99:Xt}),{22:[1,268]},t(Ft,[2,120]),t(ut,[2,94],{99:Xt}),t(ut,[2,95],{99:Xt}),t(ut,[2,96],{108:95,110:172,26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,115:tt,116:et,117:nt,118:rt,119:it,120:at}),t(ut,[2,100]),{94:[1,269]},{94:[1,270]},{51:[1,271]},{61:[1,272]},{65:[1,273]},{9:274,20:Y,21:z,23:U},t(j,[2,42]),{22:Pt,59:jt,60:Yt,79:zt,95:Ut,98:qt,100:275,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},t(Qt,[2,124]),{26:H,47:$,59:W,60:V,84:G,90:276,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},{26:H,47:$,59:W,60:V,84:G,90:277,98:X,99:Z,102:Q,104:K,105:J,108:95,110:93,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(ut,[2,104]),t(ut,[2,110]),t(st,[2,56]),{22:dt,24:pt,26:yt,38:gt,39:278,42:mt,47:$,59:W,60:V,66:vt,74:bt,76:134,77:_t,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},t(st,[2,64]),t(Ot,o,{17:279}),t(Zt,[2,122],{101:266,22:Pt,59:jt,60:Yt,79:zt,95:Ut,98:qt,102:Ht,103:$t,104:Wt,105:Vt,106:Gt}),t(ut,[2,117],{108:95,110:172,22:[1,280],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,115:tt,116:et,117:nt,118:rt,119:it,120:at}),t(ut,[2,118],{108:95,110:172,22:[1,281],26:H,47:$,59:W,60:V,84:G,98:X,99:Z,102:Q,104:K,105:J,115:tt,116:et,117:nt,118:rt,119:it,120:at}),{22:dt,24:pt,26:yt,38:gt,41:[1,282],42:mt,47:$,59:W,60:V,66:vt,74:bt,76:194,78:145,79:xt,80:wt,81:kt,82:Tt,83:Et,84:Ct,85:St,87:136,88:At,98:X,99:Z,102:Mt,104:K,105:J,106:Nt,107:Dt,108:142,115:tt,116:et,117:nt,118:rt,119:it,120:at},{18:18,19:19,20:s,21:c,22:u,23:l,32:24,33:25,34:26,35:27,36:28,37:29,38:h,42:[1,283],43:31,45:32,46:42,47:f,49:43,59:d,60:p,79:y,80:g,81:m,82:v,83:b,84:_,88:x,98:w,99:k,102:T,104:E,105:C,109:44,111:S,112:A,113:M,114:N,115:D,116:O,117:B,118:L,119:I,120:R},{22:Pt,59:jt,60:Yt,79:zt,89:284,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},{22:Pt,59:jt,60:Yt,79:zt,89:285,95:Ut,98:qt,100:235,101:236,102:Ht,103:$t,104:Wt,105:Vt,106:Gt},t(st,[2,60]),t(j,[2,41]),t(ut,[2,115],{99:Xt}),t(ut,[2,116],{99:Xt})],defaultActions:{2:[2,1],9:[2,5],10:[2,2],126:[2,7]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},Jt={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),12;case 1:return this.begin("type_directive"),13;case 2:return this.popState(),this.begin("arg_directive"),10;case 3:return this.popState(),this.popState(),15;case 4:return 14;case 5:case 6:break;case 7:this.begin("string");break;case 8:case 17:case 20:case 23:case 26:this.popState();break;case 9:return"STR";case 10:return 79;case 11:return 88;case 12:return 80;case 13:return 97;case 14:return 81;case 15:return 82;case 16:this.begin("href");break;case 18:return 93;case 19:this.begin("callbackname");break;case 21:this.popState(),this.begin("callbackargs");break;case 22:return 91;case 24:return 92;case 25:this.begin("click");break;case 27:return 83;case 28:case 29:return t.lex.firstGraph()&&this.begin("dir"),24;case 30:return 38;case 31:return 42;case 32:case 33:case 34:case 35:return 94;case 36:return this.popState(),25;case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:return this.popState(),26;case 47:return 111;case 48:return 112;case 49:return 113;case 50:return 114;case 51:return 98;case 52:return 104;case 53:return 48;case 54:return 60;case 55:return 47;case 56:return 20;case 57:return 99;case 58:return 119;case 59:case 60:case 61:return 75;case 62:case 63:case 64:return 74;case 65:return 52;case 66:return 53;case 67:return 54;case 68:return 55;case 69:return 56;case 70:return 57;case 71:return 58;case 72:return 62;case 73:return 63;case 74:return 102;case 75:return 105;case 76:return 120;case 77:return 117;case 78:return 106;case 79:case 80:return 118;case 81:return 107;case 82:return 66;case 83:return 85;case 84:return"SEP";case 85:return 84;case 86:return 59;case 87:return 68;case 88:return 67;case 89:return 70;case 90:return 69;case 91:return 115;case 92:return 116;case 93:return 61;case 94:return 50;case 95:return 51;case 96:return 40;case 97:return 41;case 98:return 64;case 99:return 65;case 100:return 126;case 101:return 21;case 102:return 22;case 103:return 23}},rules:[/^(?:%%\{)/,/^(?:((?:(?!\}%%)[^:.])*))/,/^(?::)/,/^(?:\}%%)/,/^(?:((?:(?!\}%%).|\n)*))/,/^(?:%%(?!\{)[^\n]*)/,/^(?:[^\}]%%[^\n]*)/,/^(?:["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:style\b)/,/^(?:default\b)/,/^(?:linkStyle\b)/,/^(?:interpolate\b)/,/^(?:classDef\b)/,/^(?:class\b)/,/^(?:href[\s]+["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:click[\s]+)/,/^(?:[\s\n])/,/^(?:[^\s\n]*)/,/^(?:graph\b)/,/^(?:flowchart\b)/,/^(?:subgraph\b)/,/^(?:end\b\s*)/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:(\r?\n)*\s*\n)/,/^(?:\s*LR\b)/,/^(?:\s*RL\b)/,/^(?:\s*TB\b)/,/^(?:\s*BT\b)/,/^(?:\s*TD\b)/,/^(?:\s*BR\b)/,/^(?:\s*<)/,/^(?:\s*>)/,/^(?:\s*\^)/,/^(?:\s*v\b)/,/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:[0-9]+)/,/^(?:#)/,/^(?::::)/,/^(?::)/,/^(?:&)/,/^(?:;)/,/^(?:,)/,/^(?:\*)/,/^(?:\s*[xo<]?--+[-xo>]\s*)/,/^(?:\s*[xo<]?==+[=xo>]\s*)/,/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/,/^(?:\s*[xo<]?--\s*)/,/^(?:\s*[xo<]?==\s*)/,/^(?:\s*[xo<]?-\.\s*)/,/^(?:\(-)/,/^(?:-\))/,/^(?:\(\[)/,/^(?:\]\))/,/^(?:\[\[)/,/^(?:\]\])/,/^(?:\[\|)/,/^(?:\[\()/,/^(?:\)\])/,/^(?:-)/,/^(?:\.)/,/^(?:[\_])/,/^(?:\+)/,/^(?:%)/,/^(?:=)/,/^(?:=)/,/^(?:<)/,/^(?:>)/,/^(?:\^)/,/^(?:\\\|)/,/^(?:v\b)/,/^(?:[A-Za-z]+)/,/^(?:\\\])/,/^(?:\[\/)/,/^(?:\/\])/,/^(?:\[\\)/,/^(?:[!"#$%&'*+,-.`?\\_/])/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|[\uFFD2-\uFFD7\uFFDA-\uFFDC])/,/^(?:\|)/,/^(?:\()/,/^(?:\))/,/^(?:\[)/,/^(?:\])/,/^(?:\{)/,/^(?:\})/,/^(?:")/,/^(?:(\r?\n)+)/,/^(?:\s)/,/^(?:$)/],conditions:{close_directive:{rules:[],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},open_directive:{rules:[1],inclusive:!1},callbackargs:{rules:[23,24],inclusive:!1},callbackname:{rules:[20,21,22],inclusive:!1},href:{rules:[17,18],inclusive:!1},click:{rules:[26,27],inclusive:!1},vertex:{rules:[],inclusive:!1},dir:{rules:[36,37,38,39,40,41,42,43,44,45,46],inclusive:!1},string:{rules:[8,9],inclusive:!1},INITIAL:{rules:[0,5,6,7,10,11,12,13,14,15,16,19,25,28,29,30,31,32,33,34,35,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103],inclusive:!0}}};function te(){this.yy={}}return Kt.lexer=Jt,te.prototype=Kt,Kt.Parser=te,new te}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(5354).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},9959:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,3],n=[1,5],r=[7,9,11,12,13,14,15,16,17,18,19,20,22,29,34],i=[1,15],a=[1,16],o=[1,17],s=[1,18],c=[1,19],u=[1,20],l=[1,21],h=[1,22],f=[1,23],d=[1,25],p=[1,27],y=[1,30],g=[5,7,9,11,12,13,14,15,16,17,18,19,20,22,29,34],m={trace:function(){},yy:{},symbols_:{error:2,start:3,directive:4,gantt:5,document:6,EOF:7,line:8,SPACE:9,statement:10,NL:11,dateFormat:12,inclusiveEndDates:13,topAxis:14,axisFormat:15,excludes:16,includes:17,todayMarker:18,title:19,section:20,clickStatement:21,taskTxt:22,taskData:23,openDirective:24,typeDirective:25,closeDirective:26,":":27,argDirective:28,click:29,callbackname:30,callbackargs:31,href:32,clickStatementDebug:33,open_directive:34,type_directive:35,arg_directive:36,close_directive:37,$accept:0,$end:1},terminals_:{2:"error",5:"gantt",7:"EOF",9:"SPACE",11:"NL",12:"dateFormat",13:"inclusiveEndDates",14:"topAxis",15:"axisFormat",16:"excludes",17:"includes",18:"todayMarker",19:"title",20:"section",22:"taskTxt",23:"taskData",27:":",29:"click",30:"callbackname",31:"callbackargs",32:"href",34:"open_directive",35:"type_directive",36:"arg_directive",37:"close_directive"},productions_:[0,[3,2],[3,3],[6,0],[6,2],[8,2],[8,1],[8,1],[8,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,2],[10,1],[4,4],[4,6],[21,2],[21,3],[21,3],[21,4],[21,3],[21,4],[21,2],[33,2],[33,3],[33,3],[33,4],[33,3],[33,4],[33,2],[24,1],[25,1],[28,1],[26,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 2:return a[s-1];case 3:case 7:case 8:this.$=[];break;case 4:a[s-1].push(a[s]),this.$=a[s-1];break;case 5:case 6:this.$=a[s];break;case 9:r.setDateFormat(a[s].substr(11)),this.$=a[s].substr(11);break;case 10:r.enableInclusiveEndDates(),this.$=a[s].substr(18);break;case 11:r.TopAxis(),this.$=a[s].substr(8);break;case 12:r.setAxisFormat(a[s].substr(11)),this.$=a[s].substr(11);break;case 13:r.setExcludes(a[s].substr(9)),this.$=a[s].substr(9);break;case 14:r.setIncludes(a[s].substr(9)),this.$=a[s].substr(9);break;case 15:r.setTodayMarker(a[s].substr(12)),this.$=a[s].substr(12);break;case 16:r.setTitle(a[s].substr(6)),this.$=a[s].substr(6);break;case 17:r.addSection(a[s].substr(8)),this.$=a[s].substr(8);break;case 19:r.addTask(a[s-1],a[s]),this.$="task";break;case 23:this.$=a[s-1],r.setClickEvent(a[s-1],a[s],null);break;case 24:this.$=a[s-2],r.setClickEvent(a[s-2],a[s-1],a[s]);break;case 25:this.$=a[s-2],r.setClickEvent(a[s-2],a[s-1],null),r.setLink(a[s-2],a[s]);break;case 26:this.$=a[s-3],r.setClickEvent(a[s-3],a[s-2],a[s-1]),r.setLink(a[s-3],a[s]);break;case 27:this.$=a[s-2],r.setClickEvent(a[s-2],a[s],null),r.setLink(a[s-2],a[s-1]);break;case 28:this.$=a[s-3],r.setClickEvent(a[s-3],a[s-1],a[s]),r.setLink(a[s-3],a[s-2]);break;case 29:this.$=a[s-1],r.setLink(a[s-1],a[s]);break;case 30:case 36:this.$=a[s-1]+" "+a[s];break;case 31:case 32:case 34:this.$=a[s-2]+" "+a[s-1]+" "+a[s];break;case 33:case 35:this.$=a[s-3]+" "+a[s-2]+" "+a[s-1]+" "+a[s];break;case 37:r.parseDirective("%%{","open_directive");break;case 38:r.parseDirective(a[s],"type_directive");break;case 39:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 40:r.parseDirective("}%%","close_directive","gantt")}},table:[{3:1,4:2,5:e,24:4,34:n},{1:[3]},{3:6,4:2,5:e,24:4,34:n},t(r,[2,3],{6:7}),{25:8,35:[1,9]},{35:[2,37]},{1:[2,1]},{4:26,7:[1,10],8:11,9:[1,12],10:13,11:[1,14],12:i,13:a,14:o,15:s,16:c,17:u,18:l,19:h,20:f,21:24,22:d,24:4,29:p,34:n},{26:28,27:[1,29],37:y},t([27,37],[2,38]),t(r,[2,8],{1:[2,2]}),t(r,[2,4]),{4:26,10:31,12:i,13:a,14:o,15:s,16:c,17:u,18:l,19:h,20:f,21:24,22:d,24:4,29:p,34:n},t(r,[2,6]),t(r,[2,7]),t(r,[2,9]),t(r,[2,10]),t(r,[2,11]),t(r,[2,12]),t(r,[2,13]),t(r,[2,14]),t(r,[2,15]),t(r,[2,16]),t(r,[2,17]),t(r,[2,18]),{23:[1,32]},t(r,[2,20]),{30:[1,33],32:[1,34]},{11:[1,35]},{28:36,36:[1,37]},{11:[2,40]},t(r,[2,5]),t(r,[2,19]),t(r,[2,23],{31:[1,38],32:[1,39]}),t(r,[2,29],{30:[1,40]}),t(g,[2,21]),{26:41,37:y},{37:[2,39]},t(r,[2,24],{32:[1,42]}),t(r,[2,25]),t(r,[2,27],{31:[1,43]}),{11:[1,44]},t(r,[2,26]),t(r,[2,28]),t(g,[2,22])],defaultActions:{5:[2,37],6:[2,1],30:[2,40],37:[2,39]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},v={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),34;case 1:return this.begin("type_directive"),35;case 2:return this.popState(),this.begin("arg_directive"),27;case 3:return this.popState(),this.popState(),37;case 4:return 36;case 5:case 6:case 7:case 9:case 10:case 11:break;case 8:return 11;case 12:this.begin("href");break;case 13:case 16:case 19:case 22:this.popState();break;case 14:return 32;case 15:this.begin("callbackname");break;case 17:this.popState(),this.begin("callbackargs");break;case 18:return 30;case 20:return 31;case 21:this.begin("click");break;case 23:return 29;case 24:return 5;case 25:return 12;case 26:return 13;case 27:return 14;case 28:return 15;case 29:return 17;case 30:return 16;case 31:return 18;case 32:return"date";case 33:return 19;case 34:return 20;case 35:return 22;case 36:return 23;case 37:return 27;case 38:return 7;case 39:return"INVALID"}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%%(?!\{)*[^\n]*)/i,/^(?:[^\}]%%*[^\n]*)/i,/^(?:%%*[^\n]*[\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:href[\s]+["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:call[\s]+)/i,/^(?:\([\s]*\))/i,/^(?:\()/i,/^(?:[^(]*)/i,/^(?:\))/i,/^(?:[^)]*)/i,/^(?:click[\s]+)/i,/^(?:[\s\n])/i,/^(?:[^\s\n]*)/i,/^(?:gantt\b)/i,/^(?:dateFormat\s[^#\n;]+)/i,/^(?:inclusiveEndDates\b)/i,/^(?:topAxis\b)/i,/^(?:axisFormat\s[^#\n;]+)/i,/^(?:includes\s[^#\n;]+)/i,/^(?:excludes\s[^#\n;]+)/i,/^(?:todayMarker\s[^\n;]+)/i,/^(?:\d\d\d\d-\d\d-\d\d\b)/i,/^(?:title\s[^#\n;]+)/i,/^(?:section\s[^#:\n;]+)/i,/^(?:[^#:\n;]+)/i,/^(?::[^#\n;]+)/i,/^(?::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{close_directive:{rules:[],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},open_directive:{rules:[1],inclusive:!1},callbackargs:{rules:[19,20],inclusive:!1},callbackname:{rules:[16,17,18],inclusive:!1},href:{rules:[13,14],inclusive:!1},click:{rules:[22,23],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,11,12,15,21,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39],inclusive:!0}}};function b(){this.yy={}}return m.lexer=v,b.prototype=m,m.Parser=b,new b}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(6878).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},2553:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[2,3],n=[1,7],r=[7,12,15,17,19,20,21],i=[7,11,12,15,17,19,20,21],a=[2,20],o=[1,32],s={trace:function(){},yy:{},symbols_:{error:2,start:3,GG:4,":":5,document:6,EOF:7,DIR:8,options:9,body:10,OPT:11,NL:12,line:13,statement:14,COMMIT:15,commit_arg:16,BRANCH:17,ID:18,CHECKOUT:19,MERGE:20,RESET:21,reset_arg:22,STR:23,HEAD:24,reset_parents:25,CARET:26,$accept:0,$end:1},terminals_:{2:"error",4:"GG",5:":",7:"EOF",8:"DIR",11:"OPT",12:"NL",15:"COMMIT",17:"BRANCH",18:"ID",19:"CHECKOUT",20:"MERGE",21:"RESET",23:"STR",24:"HEAD",26:"CARET"},productions_:[0,[3,4],[3,5],[6,0],[6,2],[9,2],[9,1],[10,0],[10,2],[13,2],[13,1],[14,2],[14,2],[14,2],[14,2],[14,2],[16,0],[16,1],[22,2],[22,2],[25,0],[25,2]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 1:return a[s-1];case 2:return r.setDirection(a[s-3]),a[s-1];case 4:r.setOptions(a[s-1]),this.$=a[s];break;case 5:a[s-1]+=a[s],this.$=a[s-1];break;case 7:this.$=[];break;case 8:a[s-1].push(a[s]),this.$=a[s-1];break;case 9:this.$=a[s-1];break;case 11:r.commit(a[s]);break;case 12:r.branch(a[s]);break;case 13:r.checkout(a[s]);break;case 14:r.merge(a[s]);break;case 15:r.reset(a[s]);break;case 16:this.$="";break;case 17:this.$=a[s];break;case 18:this.$=a[s-1]+":"+a[s];break;case 19:this.$=a[s-1]+":"+r.count,r.count=0;break;case 20:r.count=0;break;case 21:r.count+=1}},table:[{3:1,4:[1,2]},{1:[3]},{5:[1,3],8:[1,4]},{6:5,7:e,9:6,12:n},{5:[1,8]},{7:[1,9]},t(r,[2,7],{10:10,11:[1,11]}),t(i,[2,6]),{6:12,7:e,9:6,12:n},{1:[2,1]},{7:[2,4],12:[1,15],13:13,14:14,15:[1,16],17:[1,17],19:[1,18],20:[1,19],21:[1,20]},t(i,[2,5]),{7:[1,21]},t(r,[2,8]),{12:[1,22]},t(r,[2,10]),{12:[2,16],16:23,23:[1,24]},{18:[1,25]},{18:[1,26]},{18:[1,27]},{18:[1,30],22:28,24:[1,29]},{1:[2,2]},t(r,[2,9]),{12:[2,11]},{12:[2,17]},{12:[2,12]},{12:[2,13]},{12:[2,14]},{12:[2,15]},{12:a,25:31,26:o},{12:a,25:33,26:o},{12:[2,18]},{12:a,25:34,26:o},{12:[2,19]},{12:[2,21]}],defaultActions:{9:[2,1],21:[2,2],23:[2,11],24:[2,17],25:[2,12],26:[2,13],27:[2,14],28:[2,15],31:[2,18],33:[2,19],34:[2,21]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},c={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return 12;case 1:case 2:case 3:break;case 4:return 4;case 5:return 15;case 6:return 17;case 7:return 20;case 8:return 21;case 9:return 19;case 10:case 11:return 8;case 12:return 5;case 13:return 26;case 14:this.begin("options");break;case 15:case 18:this.popState();break;case 16:return 11;case 17:this.begin("string");break;case 19:return 23;case 20:return 18;case 21:return 7}},rules:[/^(?:(\r?\n)+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:gitGraph\b)/i,/^(?:commit\b)/i,/^(?:branch\b)/i,/^(?:merge\b)/i,/^(?:reset\b)/i,/^(?:checkout\b)/i,/^(?:LR\b)/i,/^(?:BT\b)/i,/^(?::)/i,/^(?:\^)/i,/^(?:options\r?\n)/i,/^(?:end\r?\n)/i,/^(?:[^\n]+\r?\n)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[a-zA-Z][-_\.a-zA-Z0-9]*[-_a-zA-Z0-9])/i,/^(?:$)/i],conditions:{options:{rules:[15,16],inclusive:!1},string:{rules:[18,19],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,17,20,21],inclusive:!0}}};function u(){this.yy={}}return s.lexer=c,u.prototype=s,s.Parser=u,new u}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(8183).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},6765:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[6,9,10],n={trace:function(){},yy:{},symbols_:{error:2,start:3,info:4,document:5,EOF:6,line:7,statement:8,NL:9,showInfo:10,$accept:0,$end:1},terminals_:{2:"error",4:"info",6:"EOF",9:"NL",10:"showInfo"},productions_:[0,[3,3],[5,0],[5,2],[7,1],[7,1],[8,1]],performAction:function(t,e,n,r,i,a,o){switch(a.length,i){case 1:return r;case 4:break;case 6:r.setInfo(!0)}},table:[{3:1,4:[1,2]},{1:[3]},t(e,[2,2],{5:3}),{6:[1,4],7:5,8:6,9:[1,7],10:[1,8]},{1:[2,1]},t(e,[2,3]),t(e,[2,4]),t(e,[2,5]),t(e,[2,6])],defaultActions:{4:[2,1]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},r={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return 4;case 1:return 9;case 2:return"space";case 3:return 10;case 4:return 6;case 5:return"TXT"}},rules:[/^(?:info\b)/i,/^(?:[\s\n\r]+)/i,/^(?:[\s]+)/i,/^(?:showInfo\b)/i,/^(?:$)/i,/^(?:.)/i],conditions:{INITIAL:{rules:[0,1,2,3,4,5],inclusive:!0}}};function i(){this.yy={}}return n.lexer=r,i.prototype=n,n.Parser=i,new i}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(1428).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},7062:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,4],n=[1,5],r=[1,6],i=[1,7],a=[1,9],o=[1,11,13,20,21,22,23],s=[2,5],c=[1,6,11,13,20,21,22,23],u=[20,21,22],l=[2,8],h=[1,18],f=[1,19],d=[1,24],p=[6,20,21,22,23],y={trace:function(){},yy:{},symbols_:{error:2,start:3,eol:4,directive:5,PIE:6,document:7,showData:8,line:9,statement:10,txt:11,value:12,title:13,title_value:14,openDirective:15,typeDirective:16,closeDirective:17,":":18,argDirective:19,NEWLINE:20,";":21,EOF:22,open_directive:23,type_directive:24,arg_directive:25,close_directive:26,$accept:0,$end:1},terminals_:{2:"error",6:"PIE",8:"showData",11:"txt",12:"value",13:"title",14:"title_value",18:":",20:"NEWLINE",21:";",22:"EOF",23:"open_directive",24:"type_directive",25:"arg_directive",26:"close_directive"},productions_:[0,[3,2],[3,2],[3,2],[3,3],[7,0],[7,2],[9,2],[10,0],[10,2],[10,2],[10,1],[5,3],[5,5],[4,1],[4,1],[4,1],[15,1],[16,1],[19,1],[17,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 4:r.setShowData(!0);break;case 7:this.$=a[s-1];break;case 9:r.addSection(a[s-1],r.cleanupValue(a[s]));break;case 10:this.$=a[s].trim(),r.setTitle(this.$);break;case 17:r.parseDirective("%%{","open_directive");break;case 18:r.parseDirective(a[s],"type_directive");break;case 19:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 20:r.parseDirective("}%%","close_directive","pie")}},table:[{3:1,4:2,5:3,6:e,15:8,20:n,21:r,22:i,23:a},{1:[3]},{3:10,4:2,5:3,6:e,15:8,20:n,21:r,22:i,23:a},{3:11,4:2,5:3,6:e,15:8,20:n,21:r,22:i,23:a},t(o,s,{7:12,8:[1,13]}),t(c,[2,14]),t(c,[2,15]),t(c,[2,16]),{16:14,24:[1,15]},{24:[2,17]},{1:[2,1]},{1:[2,2]},t(u,l,{15:8,9:16,10:17,5:20,1:[2,3],11:h,13:f,23:a}),t(o,s,{7:21}),{17:22,18:[1,23],26:d},t([18,26],[2,18]),t(o,[2,6]),{4:25,20:n,21:r,22:i},{12:[1,26]},{14:[1,27]},t(u,[2,11]),t(u,l,{15:8,9:16,10:17,5:20,1:[2,4],11:h,13:f,23:a}),t(p,[2,12]),{19:28,25:[1,29]},t(p,[2,20]),t(o,[2,7]),t(u,[2,9]),t(u,[2,10]),{17:30,26:d},{26:[2,19]},t(p,[2,13])],defaultActions:{9:[2,17],10:[2,1],11:[2,2],29:[2,19]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},g={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),23;case 1:return this.begin("type_directive"),24;case 2:return this.popState(),this.begin("arg_directive"),18;case 3:return this.popState(),this.popState(),26;case 4:return 25;case 5:case 6:case 8:case 9:break;case 7:return 20;case 10:return this.begin("title"),13;case 11:return this.popState(),"title_value";case 12:this.begin("string");break;case 13:this.popState();break;case 14:return"txt";case 15:return 6;case 16:return 8;case 17:return"value";case 18:return 22}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n\r]+)/i,/^(?:%%[^\n]*)/i,/^(?:[\s]+)/i,/^(?:title\b)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:pie\b)/i,/^(?:showData\b)/i,/^(?::[\s]*[\d]+(?:\.[\d]+)?)/i,/^(?:$)/i],conditions:{close_directive:{rules:[],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},open_directive:{rules:[1],inclusive:!1},title:{rules:[11],inclusive:!1},string:{rules:[13,14],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,12,15,16,17,18],inclusive:!0}}};function m(){this.yy={}}return y.lexer=g,m.prototype=y,y.Parser=m,new m}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(4551).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},3176:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,3],n=[1,5],r=[1,17],i=[2,10],a=[1,21],o=[1,22],s=[1,23],c=[1,24],u=[1,25],l=[1,26],h=[1,19],f=[1,27],d=[1,28],p=[1,31],y=[66,67],g=[5,8,14,35,36,37,38,39,40,48,55,57,66,67],m=[5,6,8,14,35,36,37,38,39,40,48,66,67],v=[1,51],b=[1,52],_=[1,53],x=[1,54],w=[1,55],k=[1,56],T=[1,57],E=[57,58],C=[1,69],S=[1,65],A=[1,66],M=[1,67],N=[1,68],D=[1,70],O=[1,74],B=[1,75],L=[1,72],I=[1,73],R=[5,8,14,35,36,37,38,39,40,48,66,67],F={trace:function(){},yy:{},symbols_:{error:2,start:3,directive:4,NEWLINE:5,RD:6,diagram:7,EOF:8,openDirective:9,typeDirective:10,closeDirective:11,":":12,argDirective:13,open_directive:14,type_directive:15,arg_directive:16,close_directive:17,requirementDef:18,elementDef:19,relationshipDef:20,requirementType:21,requirementName:22,STRUCT_START:23,requirementBody:24,ID:25,COLONSEP:26,id:27,TEXT:28,text:29,RISK:30,riskLevel:31,VERIFYMTHD:32,verifyType:33,STRUCT_STOP:34,REQUIREMENT:35,FUNCTIONAL_REQUIREMENT:36,INTERFACE_REQUIREMENT:37,PERFORMANCE_REQUIREMENT:38,PHYSICAL_REQUIREMENT:39,DESIGN_CONSTRAINT:40,LOW_RISK:41,MED_RISK:42,HIGH_RISK:43,VERIFY_ANALYSIS:44,VERIFY_DEMONSTRATION:45,VERIFY_INSPECTION:46,VERIFY_TEST:47,ELEMENT:48,elementName:49,elementBody:50,TYPE:51,type:52,DOCREF:53,ref:54,END_ARROW_L:55,relationship:56,LINE:57,END_ARROW_R:58,CONTAINS:59,COPIES:60,DERIVES:61,SATISFIES:62,VERIFIES:63,REFINES:64,TRACES:65,unqString:66,qString:67,$accept:0,$end:1},terminals_:{2:"error",5:"NEWLINE",6:"RD",8:"EOF",12:":",14:"open_directive",15:"type_directive",16:"arg_directive",17:"close_directive",23:"STRUCT_START",25:"ID",26:"COLONSEP",28:"TEXT",30:"RISK",32:"VERIFYMTHD",34:"STRUCT_STOP",35:"REQUIREMENT",36:"FUNCTIONAL_REQUIREMENT",37:"INTERFACE_REQUIREMENT",38:"PERFORMANCE_REQUIREMENT",39:"PHYSICAL_REQUIREMENT",40:"DESIGN_CONSTRAINT",41:"LOW_RISK",42:"MED_RISK",43:"HIGH_RISK",44:"VERIFY_ANALYSIS",45:"VERIFY_DEMONSTRATION",46:"VERIFY_INSPECTION",47:"VERIFY_TEST",48:"ELEMENT",51:"TYPE",53:"DOCREF",55:"END_ARROW_L",57:"LINE",58:"END_ARROW_R",59:"CONTAINS",60:"COPIES",61:"DERIVES",62:"SATISFIES",63:"VERIFIES",64:"REFINES",65:"TRACES",66:"unqString",67:"qString"},productions_:[0,[3,3],[3,2],[3,4],[4,3],[4,5],[9,1],[10,1],[13,1],[11,1],[7,0],[7,2],[7,2],[7,2],[7,2],[7,2],[18,5],[24,5],[24,5],[24,5],[24,5],[24,2],[24,1],[21,1],[21,1],[21,1],[21,1],[21,1],[21,1],[31,1],[31,1],[31,1],[33,1],[33,1],[33,1],[33,1],[19,5],[50,5],[50,5],[50,2],[50,1],[20,5],[20,5],[56,1],[56,1],[56,1],[56,1],[56,1],[56,1],[56,1],[22,1],[22,1],[27,1],[27,1],[29,1],[29,1],[49,1],[49,1],[52,1],[52,1],[54,1],[54,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 6:r.parseDirective("%%{","open_directive");break;case 7:r.parseDirective(a[s],"type_directive");break;case 8:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 9:r.parseDirective("}%%","close_directive","pie");break;case 10:this.$=[];break;case 16:r.addRequirement(a[s-3],a[s-4]);break;case 17:r.setNewReqId(a[s-2]);break;case 18:r.setNewReqText(a[s-2]);break;case 19:r.setNewReqRisk(a[s-2]);break;case 20:r.setNewReqVerifyMethod(a[s-2]);break;case 23:this.$=r.RequirementType.REQUIREMENT;break;case 24:this.$=r.RequirementType.FUNCTIONAL_REQUIREMENT;break;case 25:this.$=r.RequirementType.INTERFACE_REQUIREMENT;break;case 26:this.$=r.RequirementType.PERFORMANCE_REQUIREMENT;break;case 27:this.$=r.RequirementType.PHYSICAL_REQUIREMENT;break;case 28:this.$=r.RequirementType.DESIGN_CONSTRAINT;break;case 29:this.$=r.RiskLevel.LOW_RISK;break;case 30:this.$=r.RiskLevel.MED_RISK;break;case 31:this.$=r.RiskLevel.HIGH_RISK;break;case 32:this.$=r.VerifyType.VERIFY_ANALYSIS;break;case 33:this.$=r.VerifyType.VERIFY_DEMONSTRATION;break;case 34:this.$=r.VerifyType.VERIFY_INSPECTION;break;case 35:this.$=r.VerifyType.VERIFY_TEST;break;case 36:r.addElement(a[s-3]);break;case 37:r.setNewElementType(a[s-2]);break;case 38:r.setNewElementDocRef(a[s-2]);break;case 41:r.addRelationship(a[s-2],a[s],a[s-4]);break;case 42:r.addRelationship(a[s-2],a[s-4],a[s]);break;case 43:this.$=r.Relationships.CONTAINS;break;case 44:this.$=r.Relationships.COPIES;break;case 45:this.$=r.Relationships.DERIVES;break;case 46:this.$=r.Relationships.SATISFIES;break;case 47:this.$=r.Relationships.VERIFIES;break;case 48:this.$=r.Relationships.REFINES;break;case 49:this.$=r.Relationships.TRACES}},table:[{3:1,4:2,6:e,9:4,14:n},{1:[3]},{3:7,4:2,5:[1,6],6:e,9:4,14:n},{5:[1,8]},{10:9,15:[1,10]},{15:[2,6]},{3:11,4:2,6:e,9:4,14:n},{1:[2,2]},{4:16,5:r,7:12,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{11:29,12:[1,30],17:p},t([12,17],[2,7]),{1:[2,1]},{8:[1,32]},{4:16,5:r,7:33,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{4:16,5:r,7:34,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{4:16,5:r,7:35,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{4:16,5:r,7:36,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{4:16,5:r,7:37,8:i,9:4,14:n,18:13,19:14,20:15,21:18,27:20,35:a,36:o,37:s,38:c,39:u,40:l,48:h,66:f,67:d},{22:38,66:[1,39],67:[1,40]},{49:41,66:[1,42],67:[1,43]},{55:[1,44],57:[1,45]},t(y,[2,23]),t(y,[2,24]),t(y,[2,25]),t(y,[2,26]),t(y,[2,27]),t(y,[2,28]),t(g,[2,52]),t(g,[2,53]),t(m,[2,4]),{13:46,16:[1,47]},t(m,[2,9]),{1:[2,3]},{8:[2,11]},{8:[2,12]},{8:[2,13]},{8:[2,14]},{8:[2,15]},{23:[1,48]},{23:[2,50]},{23:[2,51]},{23:[1,49]},{23:[2,56]},{23:[2,57]},{56:50,59:v,60:b,61:_,62:x,63:w,64:k,65:T},{56:58,59:v,60:b,61:_,62:x,63:w,64:k,65:T},{11:59,17:p},{17:[2,8]},{5:[1,60]},{5:[1,61]},{57:[1,62]},t(E,[2,43]),t(E,[2,44]),t(E,[2,45]),t(E,[2,46]),t(E,[2,47]),t(E,[2,48]),t(E,[2,49]),{58:[1,63]},t(m,[2,5]),{5:C,24:64,25:S,28:A,30:M,32:N,34:D},{5:O,34:B,50:71,51:L,53:I},{27:76,66:f,67:d},{27:77,66:f,67:d},t(R,[2,16]),{26:[1,78]},{26:[1,79]},{26:[1,80]},{26:[1,81]},{5:C,24:82,25:S,28:A,30:M,32:N,34:D},t(R,[2,22]),t(R,[2,36]),{26:[1,83]},{26:[1,84]},{5:O,34:B,50:85,51:L,53:I},t(R,[2,40]),t(R,[2,41]),t(R,[2,42]),{27:86,66:f,67:d},{29:87,66:[1,88],67:[1,89]},{31:90,41:[1,91],42:[1,92],43:[1,93]},{33:94,44:[1,95],45:[1,96],46:[1,97],47:[1,98]},t(R,[2,21]),{52:99,66:[1,100],67:[1,101]},{54:102,66:[1,103],67:[1,104]},t(R,[2,39]),{5:[1,105]},{5:[1,106]},{5:[2,54]},{5:[2,55]},{5:[1,107]},{5:[2,29]},{5:[2,30]},{5:[2,31]},{5:[1,108]},{5:[2,32]},{5:[2,33]},{5:[2,34]},{5:[2,35]},{5:[1,109]},{5:[2,58]},{5:[2,59]},{5:[1,110]},{5:[2,60]},{5:[2,61]},{5:C,24:111,25:S,28:A,30:M,32:N,34:D},{5:C,24:112,25:S,28:A,30:M,32:N,34:D},{5:C,24:113,25:S,28:A,30:M,32:N,34:D},{5:C,24:114,25:S,28:A,30:M,32:N,34:D},{5:O,34:B,50:115,51:L,53:I},{5:O,34:B,50:116,51:L,53:I},t(R,[2,17]),t(R,[2,18]),t(R,[2,19]),t(R,[2,20]),t(R,[2,37]),t(R,[2,38])],defaultActions:{5:[2,6],7:[2,2],11:[2,1],32:[2,3],33:[2,11],34:[2,12],35:[2,13],36:[2,14],37:[2,15],39:[2,50],40:[2,51],42:[2,56],43:[2,57],47:[2,8],88:[2,54],89:[2,55],91:[2,29],92:[2,30],93:[2,31],95:[2,32],96:[2,33],97:[2,34],98:[2,35],100:[2,58],101:[2,59],103:[2,60],104:[2,61]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},P={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),14;case 1:return this.begin("type_directive"),15;case 2:return this.popState(),this.begin("arg_directive"),12;case 3:return this.popState(),this.popState(),17;case 4:return 16;case 5:return 5;case 6:case 7:case 8:break;case 9:return 8;case 10:return 6;case 11:return 23;case 12:return 34;case 13:return 26;case 14:return 25;case 15:return 28;case 16:return 30;case 17:return 32;case 18:return 35;case 19:return 36;case 20:return 37;case 21:return 38;case 22:return 39;case 23:return 40;case 24:return 41;case 25:return 42;case 26:return 43;case 27:return 44;case 28:return 45;case 29:return 46;case 30:return 47;case 31:return 48;case 32:return 59;case 33:return 60;case 34:return 61;case 35:return 62;case 36:return 63;case 37:return 64;case 38:return 65;case 39:return 51;case 40:return 53;case 41:return 55;case 42:return 58;case 43:return 57;case 44:this.begin("string");break;case 45:this.popState();break;case 46:return"qString";case 47:return e.yytext=e.yytext.trim(),66}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:(\r?\n)+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:$)/i,/^(?:requirementDiagram\b)/i,/^(?:\{)/i,/^(?:\})/i,/^(?::)/i,/^(?:id\b)/i,/^(?:text\b)/i,/^(?:risk\b)/i,/^(?:verifyMethod\b)/i,/^(?:requirement\b)/i,/^(?:functionalRequirement\b)/i,/^(?:interfaceRequirement\b)/i,/^(?:performanceRequirement\b)/i,/^(?:physicalRequirement\b)/i,/^(?:designConstraint\b)/i,/^(?:low\b)/i,/^(?:medium\b)/i,/^(?:high\b)/i,/^(?:analysis\b)/i,/^(?:demonstration\b)/i,/^(?:inspection\b)/i,/^(?:test\b)/i,/^(?:element\b)/i,/^(?:contains\b)/i,/^(?:copies\b)/i,/^(?:derives\b)/i,/^(?:satisfies\b)/i,/^(?:verifies\b)/i,/^(?:refines\b)/i,/^(?:traces\b)/i,/^(?:type\b)/i,/^(?:docref\b)/i,/^(?:<-)/i,/^(?:->)/i,/^(?:-)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[\w][^\r\n\{\<\>\-\=]*)/i],conditions:{close_directive:{rules:[],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},open_directive:{rules:[1],inclusive:!1},unqString:{rules:[],inclusive:!1},token:{rules:[],inclusive:!1},string:{rules:[45,46],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,47],inclusive:!0}}};function j(){this.yy={}}return F.lexer=P,j.prototype=F,F.Parser=j,new j}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(8800).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},6876:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,2],n=[1,3],r=[1,5],i=[1,7],a=[2,5],o=[1,15],s=[1,17],c=[1,18],u=[1,19],l=[1,21],h=[1,22],f=[1,23],d=[1,29],p=[1,30],y=[1,31],g=[1,32],m=[1,33],v=[1,34],b=[1,37],_=[1,38],x=[1,39],w=[1,40],k=[1,41],T=[1,42],E=[1,45],C=[1,4,5,16,20,22,23,24,30,32,33,34,35,36,38,40,41,42,46,47,48,49,57,67],S=[1,58],A=[4,5,16,20,22,23,24,30,32,33,34,35,36,38,42,46,47,48,49,57,67],M=[4,5,16,20,22,23,24,30,32,33,34,35,36,38,41,42,46,47,48,49,57,67],N=[4,5,16,20,22,23,24,30,32,33,34,35,36,38,40,42,46,47,48,49,57,67],D=[55,56,57],O=[1,4,5,7,16,20,22,23,24,30,32,33,34,35,36,38,40,41,42,46,47,48,49,57,67],B={trace:function(){},yy:{},symbols_:{error:2,start:3,SPACE:4,NEWLINE:5,directive:6,SD:7,document:8,line:9,statement:10,openDirective:11,typeDirective:12,closeDirective:13,":":14,argDirective:15,participant:16,actor:17,AS:18,restOfLine:19,participant_actor:20,signal:21,autonumber:22,activate:23,deactivate:24,note_statement:25,links_statement:26,link_statement:27,properties_statement:28,details_statement:29,title:30,text2:31,loop:32,end:33,rect:34,opt:35,alt:36,else_sections:37,par:38,par_sections:39,and:40,else:41,note:42,placement:43,over:44,actor_pair:45,links:46,link:47,properties:48,details:49,spaceList:50,",":51,left_of:52,right_of:53,signaltype:54,"+":55,"-":56,ACTOR:57,SOLID_OPEN_ARROW:58,DOTTED_OPEN_ARROW:59,SOLID_ARROW:60,DOTTED_ARROW:61,SOLID_CROSS:62,DOTTED_CROSS:63,SOLID_POINT:64,DOTTED_POINT:65,TXT:66,open_directive:67,type_directive:68,arg_directive:69,close_directive:70,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NEWLINE",7:"SD",14:":",16:"participant",18:"AS",19:"restOfLine",20:"participant_actor",22:"autonumber",23:"activate",24:"deactivate",30:"title",32:"loop",33:"end",34:"rect",35:"opt",36:"alt",38:"par",40:"and",41:"else",42:"note",44:"over",46:"links",47:"link",48:"properties",49:"details",51:",",52:"left_of",53:"right_of",55:"+",56:"-",57:"ACTOR",58:"SOLID_OPEN_ARROW",59:"DOTTED_OPEN_ARROW",60:"SOLID_ARROW",61:"DOTTED_ARROW",62:"SOLID_CROSS",63:"DOTTED_CROSS",64:"SOLID_POINT",65:"DOTTED_POINT",66:"TXT",67:"open_directive",68:"type_directive",69:"arg_directive",70:"close_directive"},productions_:[0,[3,2],[3,2],[3,2],[3,2],[8,0],[8,2],[9,2],[9,1],[9,1],[6,4],[6,6],[10,5],[10,3],[10,5],[10,3],[10,2],[10,1],[10,3],[10,3],[10,2],[10,2],[10,2],[10,2],[10,2],[10,3],[10,4],[10,4],[10,4],[10,4],[10,4],[10,1],[39,1],[39,4],[37,1],[37,4],[25,4],[25,4],[26,3],[27,3],[28,3],[29,3],[50,2],[50,1],[45,3],[45,1],[43,1],[43,1],[21,5],[21,5],[21,4],[17,1],[54,1],[54,1],[54,1],[54,1],[54,1],[54,1],[54,1],[54,1],[31,1],[11,1],[12,1],[15,1],[13,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 4:return r.apply(a[s]),a[s];case 5:case 9:this.$=[];break;case 6:a[s-1].push(a[s]),this.$=a[s-1];break;case 7:case 8:case 45:this.$=a[s];break;case 12:a[s-3].type="addParticipant",a[s-3].description=r.parseMessage(a[s-1]),this.$=a[s-3];break;case 13:a[s-1].type="addParticipant",this.$=a[s-1];break;case 14:a[s-3].type="addActor",a[s-3].description=r.parseMessage(a[s-1]),this.$=a[s-3];break;case 15:a[s-1].type="addActor",this.$=a[s-1];break;case 17:r.enableSequenceNumbers();break;case 18:this.$={type:"activeStart",signalType:r.LINETYPE.ACTIVE_START,actor:a[s-1]};break;case 19:this.$={type:"activeEnd",signalType:r.LINETYPE.ACTIVE_END,actor:a[s-1]};break;case 25:this.$=[{type:"setTitle",text:a[s-1]}];break;case 26:a[s-1].unshift({type:"loopStart",loopText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.LOOP_START}),a[s-1].push({type:"loopEnd",loopText:a[s-2],signalType:r.LINETYPE.LOOP_END}),this.$=a[s-1];break;case 27:a[s-1].unshift({type:"rectStart",color:r.parseMessage(a[s-2]),signalType:r.LINETYPE.RECT_START}),a[s-1].push({type:"rectEnd",color:r.parseMessage(a[s-2]),signalType:r.LINETYPE.RECT_END}),this.$=a[s-1];break;case 28:a[s-1].unshift({type:"optStart",optText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.OPT_START}),a[s-1].push({type:"optEnd",optText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.OPT_END}),this.$=a[s-1];break;case 29:a[s-1].unshift({type:"altStart",altText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.ALT_START}),a[s-1].push({type:"altEnd",signalType:r.LINETYPE.ALT_END}),this.$=a[s-1];break;case 30:a[s-1].unshift({type:"parStart",parText:r.parseMessage(a[s-2]),signalType:r.LINETYPE.PAR_START}),a[s-1].push({type:"parEnd",signalType:r.LINETYPE.PAR_END}),this.$=a[s-1];break;case 33:this.$=a[s-3].concat([{type:"and",parText:r.parseMessage(a[s-1]),signalType:r.LINETYPE.PAR_AND},a[s]]);break;case 35:this.$=a[s-3].concat([{type:"else",altText:r.parseMessage(a[s-1]),signalType:r.LINETYPE.ALT_ELSE},a[s]]);break;case 36:this.$=[a[s-1],{type:"addNote",placement:a[s-2],actor:a[s-1].actor,text:a[s]}];break;case 37:a[s-2]=[].concat(a[s-1],a[s-1]).slice(0,2),a[s-2][0]=a[s-2][0].actor,a[s-2][1]=a[s-2][1].actor,this.$=[a[s-1],{type:"addNote",placement:r.PLACEMENT.OVER,actor:a[s-2].slice(0,2),text:a[s]}];break;case 38:this.$=[a[s-1],{type:"addLinks",actor:a[s-1].actor,text:a[s]}];break;case 39:this.$=[a[s-1],{type:"addALink",actor:a[s-1].actor,text:a[s]}];break;case 40:this.$=[a[s-1],{type:"addProperties",actor:a[s-1].actor,text:a[s]}];break;case 41:this.$=[a[s-1],{type:"addDetails",actor:a[s-1].actor,text:a[s]}];break;case 44:this.$=[a[s-2],a[s]];break;case 46:this.$=r.PLACEMENT.LEFTOF;break;case 47:this.$=r.PLACEMENT.RIGHTOF;break;case 48:this.$=[a[s-4],a[s-1],{type:"addMessage",from:a[s-4].actor,to:a[s-1].actor,signalType:a[s-3],msg:a[s]},{type:"activeStart",signalType:r.LINETYPE.ACTIVE_START,actor:a[s-1]}];break;case 49:this.$=[a[s-4],a[s-1],{type:"addMessage",from:a[s-4].actor,to:a[s-1].actor,signalType:a[s-3],msg:a[s]},{type:"activeEnd",signalType:r.LINETYPE.ACTIVE_END,actor:a[s-4]}];break;case 50:this.$=[a[s-3],a[s-1],{type:"addMessage",from:a[s-3].actor,to:a[s-1].actor,signalType:a[s-2],msg:a[s]}];break;case 51:this.$={type:"addParticipant",actor:a[s]};break;case 52:this.$=r.LINETYPE.SOLID_OPEN;break;case 53:this.$=r.LINETYPE.DOTTED_OPEN;break;case 54:this.$=r.LINETYPE.SOLID;break;case 55:this.$=r.LINETYPE.DOTTED;break;case 56:this.$=r.LINETYPE.SOLID_CROSS;break;case 57:this.$=r.LINETYPE.DOTTED_CROSS;break;case 58:this.$=r.LINETYPE.SOLID_POINT;break;case 59:this.$=r.LINETYPE.DOTTED_POINT;break;case 60:this.$=r.parseMessage(a[s].trim().substring(1));break;case 61:r.parseDirective("%%{","open_directive");break;case 62:r.parseDirective(a[s],"type_directive");break;case 63:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 64:r.parseDirective("}%%","close_directive","sequence")}},table:[{3:1,4:e,5:n,6:4,7:r,11:6,67:i},{1:[3]},{3:8,4:e,5:n,6:4,7:r,11:6,67:i},{3:9,4:e,5:n,6:4,7:r,11:6,67:i},{3:10,4:e,5:n,6:4,7:r,11:6,67:i},t([1,4,5,16,20,22,23,24,30,32,34,35,36,38,42,46,47,48,49,57,67],a,{8:11}),{12:12,68:[1,13]},{68:[2,61]},{1:[2,1]},{1:[2,2]},{1:[2,3]},{1:[2,4],4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},{13:43,14:[1,44],70:E},t([14,70],[2,62]),t(C,[2,6]),{6:35,10:46,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},t(C,[2,8]),t(C,[2,9]),{17:47,57:T},{17:48,57:T},{5:[1,49]},t(C,[2,17]),{17:50,57:T},{17:51,57:T},{5:[1,52]},{5:[1,53]},{5:[1,54]},{5:[1,55]},{5:[1,56]},{31:57,66:S},{19:[1,59]},{19:[1,60]},{19:[1,61]},{19:[1,62]},{19:[1,63]},t(C,[2,31]),{54:64,58:[1,65],59:[1,66],60:[1,67],61:[1,68],62:[1,69],63:[1,70],64:[1,71],65:[1,72]},{43:73,44:[1,74],52:[1,75],53:[1,76]},{17:77,57:T},{17:78,57:T},{17:79,57:T},{17:80,57:T},t([5,18,51,58,59,60,61,62,63,64,65,66],[2,51]),{5:[1,81]},{15:82,69:[1,83]},{5:[2,64]},t(C,[2,7]),{5:[1,85],18:[1,84]},{5:[1,87],18:[1,86]},t(C,[2,16]),{5:[1,88]},{5:[1,89]},t(C,[2,20]),t(C,[2,21]),t(C,[2,22]),t(C,[2,23]),t(C,[2,24]),{5:[1,90]},{5:[2,60]},t(A,a,{8:91}),t(A,a,{8:92}),t(A,a,{8:93}),t(M,a,{37:94,8:95}),t(N,a,{39:96,8:97}),{17:100,55:[1,98],56:[1,99],57:T},t(D,[2,52]),t(D,[2,53]),t(D,[2,54]),t(D,[2,55]),t(D,[2,56]),t(D,[2,57]),t(D,[2,58]),t(D,[2,59]),{17:101,57:T},{17:103,45:102,57:T},{57:[2,46]},{57:[2,47]},{31:104,66:S},{31:105,66:S},{31:106,66:S},{31:107,66:S},t(O,[2,10]),{13:108,70:E},{70:[2,63]},{19:[1,109]},t(C,[2,13]),{19:[1,110]},t(C,[2,15]),t(C,[2,18]),t(C,[2,19]),t(C,[2,25]),{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[1,111],34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[1,112],34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[1,113],34:y,35:g,36:m,38:v,42:b,46:_,47:x,48:w,49:k,57:T,67:i},{33:[1,114]},{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[2,34],34:y,35:g,36:m,38:v,41:[1,115],42:b,46:_,47:x,48:w,49:k,57:T,67:i},{33:[1,116]},{4:o,5:s,6:35,9:14,10:16,11:6,16:c,17:36,20:u,21:20,22:l,23:h,24:f,25:24,26:25,27:26,28:27,29:28,30:d,32:p,33:[2,32],34:y,35:g,36:m,38:v,40:[1,117],42:b,46:_,47:x,48:w,49:k,57:T,67:i},{17:118,57:T},{17:119,57:T},{31:120,66:S},{31:121,66:S},{31:122,66:S},{51:[1,123],66:[2,45]},{5:[2,38]},{5:[2,39]},{5:[2,40]},{5:[2,41]},{5:[1,124]},{5:[1,125]},{5:[1,126]},t(C,[2,26]),t(C,[2,27]),t(C,[2,28]),t(C,[2,29]),{19:[1,127]},t(C,[2,30]),{19:[1,128]},{31:129,66:S},{31:130,66:S},{5:[2,50]},{5:[2,36]},{5:[2,37]},{17:131,57:T},t(O,[2,11]),t(C,[2,12]),t(C,[2,14]),t(M,a,{8:95,37:132}),t(N,a,{8:97,39:133}),{5:[2,48]},{5:[2,49]},{66:[2,44]},{33:[2,35]},{33:[2,33]}],defaultActions:{7:[2,61],8:[2,1],9:[2,2],10:[2,3],45:[2,64],58:[2,60],75:[2,46],76:[2,47],83:[2,63],104:[2,38],105:[2,39],106:[2,40],107:[2,41],120:[2,50],121:[2,36],122:[2,37],129:[2,48],130:[2,49],131:[2,44],132:[2,35],133:[2,33]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},L={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),67;case 1:return this.begin("type_directive"),68;case 2:return this.popState(),this.begin("arg_directive"),14;case 3:return this.popState(),this.popState(),70;case 4:return 69;case 5:case 39:case 52:return 5;case 6:case 7:case 8:case 9:case 10:break;case 11:return this.begin("ID"),16;case 12:return this.begin("ID"),20;case 13:return e.yytext=e.yytext.trim(),this.begin("ALIAS"),57;case 14:return this.popState(),this.popState(),this.begin("LINE"),18;case 15:return this.popState(),this.popState(),5;case 16:return this.begin("LINE"),32;case 17:return this.begin("LINE"),34;case 18:return this.begin("LINE"),35;case 19:return this.begin("LINE"),36;case 20:return this.begin("LINE"),41;case 21:return this.begin("LINE"),38;case 22:return this.begin("LINE"),40;case 23:return this.popState(),19;case 24:return 33;case 25:return 52;case 26:return 53;case 27:return 46;case 28:return 47;case 29:return 48;case 30:return 49;case 31:return 44;case 32:return 42;case 33:return this.begin("ID"),23;case 34:return this.begin("ID"),24;case 35:return 30;case 36:return 7;case 37:return 22;case 38:return 51;case 40:return e.yytext=e.yytext.trim(),57;case 41:return 60;case 42:return 61;case 43:return 58;case 44:return 59;case 45:return 62;case 46:return 63;case 47:return 64;case 48:return 65;case 49:return 66;case 50:return 55;case 51:return 56;case 53:return"INVALID"}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:participant\b)/i,/^(?:actor\b)/i,/^(?:[^\->:\n,;]+?(?=((?!\n)\s)+as(?!\n)\s|[#\n;]|$))/i,/^(?:as\b)/i,/^(?:(?:))/i,/^(?:loop\b)/i,/^(?:rect\b)/i,/^(?:opt\b)/i,/^(?:alt\b)/i,/^(?:else\b)/i,/^(?:par\b)/i,/^(?:and\b)/i,/^(?:(?:[:]?(?:no)?wrap)?[^#\n;]*)/i,/^(?:end\b)/i,/^(?:left of\b)/i,/^(?:right of\b)/i,/^(?:links\b)/i,/^(?:link\b)/i,/^(?:properties\b)/i,/^(?:details\b)/i,/^(?:over\b)/i,/^(?:note\b)/i,/^(?:activate\b)/i,/^(?:deactivate\b)/i,/^(?:title\b)/i,/^(?:sequenceDiagram\b)/i,/^(?:autonumber\b)/i,/^(?:,)/i,/^(?:;)/i,/^(?:[^\+\->:\n,;]+((?!(-x|--x|-\)|--\)))[\-]*[^\+\->:\n,;]+)*)/i,/^(?:->>)/i,/^(?:-->>)/i,/^(?:->)/i,/^(?:-->)/i,/^(?:-[x])/i,/^(?:--[x])/i,/^(?:-[\)])/i,/^(?:--[\)])/i,/^(?::(?:(?:no)?wrap)?[^#\n;]+)/i,/^(?:\+)/i,/^(?:-)/i,/^(?:$)/i,/^(?:.)/i],conditions:{open_directive:{rules:[1,8],inclusive:!1},type_directive:{rules:[2,3,8],inclusive:!1},arg_directive:{rules:[3,4,8],inclusive:!1},ID:{rules:[7,8,13],inclusive:!1},ALIAS:{rules:[7,8,14,15],inclusive:!1},LINE:{rules:[7,8,23],inclusive:!1},INITIAL:{rules:[0,5,6,8,9,10,11,12,16,17,18,19,20,21,22,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53],inclusive:!0}}};function I(){this.yy={}}return B.lexer=L,I.prototype=B,B.Parser=I,new I}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(1993).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},3584:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,2],n=[1,3],r=[1,5],i=[1,7],a=[2,5],o=[1,15],s=[1,17],c=[1,19],u=[1,20],l=[1,21],h=[1,22],f=[1,30],d=[1,23],p=[1,24],y=[1,25],g=[1,26],m=[1,27],v=[1,32],b=[1,33],_=[1,34],x=[1,35],w=[1,31],k=[1,38],T=[1,4,5,14,15,17,19,20,22,23,24,25,26,27,36,37,38,39,42,45],E=[1,4,5,12,13,14,15,17,19,20,22,23,24,25,26,27,36,37,38,39,42,45],C=[1,4,5,7,14,15,17,19,20,22,23,24,25,26,27,36,37,38,39,42,45],S=[4,5,14,15,17,19,20,22,23,24,25,26,27,36,37,38,39,42,45],A={trace:function(){},yy:{},symbols_:{error:2,start:3,SPACE:4,NL:5,directive:6,SD:7,document:8,line:9,statement:10,idStatement:11,DESCR:12,"--\x3e":13,HIDE_EMPTY:14,scale:15,WIDTH:16,COMPOSIT_STATE:17,STRUCT_START:18,STRUCT_STOP:19,STATE_DESCR:20,AS:21,ID:22,FORK:23,JOIN:24,CHOICE:25,CONCURRENT:26,note:27,notePosition:28,NOTE_TEXT:29,direction:30,openDirective:31,typeDirective:32,closeDirective:33,":":34,argDirective:35,direction_tb:36,direction_bt:37,direction_rl:38,direction_lr:39,eol:40,";":41,EDGE_STATE:42,left_of:43,right_of:44,open_directive:45,type_directive:46,arg_directive:47,close_directive:48,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NL",7:"SD",12:"DESCR",13:"--\x3e",14:"HIDE_EMPTY",15:"scale",16:"WIDTH",17:"COMPOSIT_STATE",18:"STRUCT_START",19:"STRUCT_STOP",20:"STATE_DESCR",21:"AS",22:"ID",23:"FORK",24:"JOIN",25:"CHOICE",26:"CONCURRENT",27:"note",29:"NOTE_TEXT",34:":",36:"direction_tb",37:"direction_bt",38:"direction_rl",39:"direction_lr",41:";",42:"EDGE_STATE",43:"left_of",44:"right_of",45:"open_directive",46:"type_directive",47:"arg_directive",48:"close_directive"},productions_:[0,[3,2],[3,2],[3,2],[3,2],[8,0],[8,2],[9,2],[9,1],[9,1],[10,1],[10,2],[10,3],[10,4],[10,1],[10,2],[10,1],[10,4],[10,3],[10,6],[10,1],[10,1],[10,1],[10,1],[10,4],[10,4],[10,1],[10,1],[6,3],[6,5],[30,1],[30,1],[30,1],[30,1],[40,1],[40,1],[11,1],[11,1],[28,1],[28,1],[31,1],[32,1],[35,1],[33,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 4:return r.setRootDoc(a[s]),a[s];case 5:this.$=[];break;case 6:"nl"!=a[s]&&(a[s-1].push(a[s]),this.$=a[s-1]);break;case 7:case 8:case 36:case 37:this.$=a[s];break;case 9:this.$="nl";break;case 10:this.$={stmt:"state",id:a[s],type:"default",description:""};break;case 11:this.$={stmt:"state",id:a[s-1],type:"default",description:r.trimColon(a[s])};break;case 12:this.$={stmt:"relation",state1:{stmt:"state",id:a[s-2],type:"default",description:""},state2:{stmt:"state",id:a[s],type:"default",description:""}};break;case 13:this.$={stmt:"relation",state1:{stmt:"state",id:a[s-3],type:"default",description:""},state2:{stmt:"state",id:a[s-1],type:"default",description:""},description:a[s].substr(1).trim()};break;case 17:this.$={stmt:"state",id:a[s-3],type:"default",description:"",doc:a[s-1]};break;case 18:var c=a[s],u=a[s-2].trim();if(a[s].match(":")){var l=a[s].split(":");c=l[0],u=[u,l[1]]}this.$={stmt:"state",id:c,type:"default",description:u};break;case 19:this.$={stmt:"state",id:a[s-3],type:"default",description:a[s-5],doc:a[s-1]};break;case 20:this.$={stmt:"state",id:a[s],type:"fork"};break;case 21:this.$={stmt:"state",id:a[s],type:"join"};break;case 22:this.$={stmt:"state",id:a[s],type:"choice"};break;case 23:this.$={stmt:"state",id:r.getDividerId(),type:"divider"};break;case 24:this.$={stmt:"state",id:a[s-1].trim(),note:{position:a[s-2].trim(),text:a[s].trim()}};break;case 30:r.setDirection("TB"),this.$={stmt:"dir",value:"TB"};break;case 31:r.setDirection("BT"),this.$={stmt:"dir",value:"BT"};break;case 32:r.setDirection("RL"),this.$={stmt:"dir",value:"RL"};break;case 33:r.setDirection("LR"),this.$={stmt:"dir",value:"LR"};break;case 40:r.parseDirective("%%{","open_directive");break;case 41:r.parseDirective(a[s],"type_directive");break;case 42:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 43:r.parseDirective("}%%","close_directive","state")}},table:[{3:1,4:e,5:n,6:4,7:r,31:6,45:i},{1:[3]},{3:8,4:e,5:n,6:4,7:r,31:6,45:i},{3:9,4:e,5:n,6:4,7:r,31:6,45:i},{3:10,4:e,5:n,6:4,7:r,31:6,45:i},t([1,4,5,14,15,17,20,22,23,24,25,26,27,36,37,38,39,42,45],a,{8:11}),{32:12,46:[1,13]},{46:[2,40]},{1:[2,1]},{1:[2,2]},{1:[2,3]},{1:[2,4],4:o,5:s,6:28,9:14,10:16,11:18,14:c,15:u,17:l,20:h,22:f,23:d,24:p,25:y,26:g,27:m,30:29,31:6,36:v,37:b,38:_,39:x,42:w,45:i},{33:36,34:[1,37],48:k},t([34,48],[2,41]),t(T,[2,6]),{6:28,10:39,11:18,14:c,15:u,17:l,20:h,22:f,23:d,24:p,25:y,26:g,27:m,30:29,31:6,36:v,37:b,38:_,39:x,42:w,45:i},t(T,[2,8]),t(T,[2,9]),t(T,[2,10],{12:[1,40],13:[1,41]}),t(T,[2,14]),{16:[1,42]},t(T,[2,16],{18:[1,43]}),{21:[1,44]},t(T,[2,20]),t(T,[2,21]),t(T,[2,22]),t(T,[2,23]),{28:45,29:[1,46],43:[1,47],44:[1,48]},t(T,[2,26]),t(T,[2,27]),t(E,[2,36]),t(E,[2,37]),t(T,[2,30]),t(T,[2,31]),t(T,[2,32]),t(T,[2,33]),t(C,[2,28]),{35:49,47:[1,50]},t(C,[2,43]),t(T,[2,7]),t(T,[2,11]),{11:51,22:f,42:w},t(T,[2,15]),t(S,a,{8:52}),{22:[1,53]},{22:[1,54]},{21:[1,55]},{22:[2,38]},{22:[2,39]},{33:56,48:k},{48:[2,42]},t(T,[2,12],{12:[1,57]}),{4:o,5:s,6:28,9:14,10:16,11:18,14:c,15:u,17:l,19:[1,58],20:h,22:f,23:d,24:p,25:y,26:g,27:m,30:29,31:6,36:v,37:b,38:_,39:x,42:w,45:i},t(T,[2,18],{18:[1,59]}),{29:[1,60]},{22:[1,61]},t(C,[2,29]),t(T,[2,13]),t(T,[2,17]),t(S,a,{8:62}),t(T,[2,24]),t(T,[2,25]),{4:o,5:s,6:28,9:14,10:16,11:18,14:c,15:u,17:l,19:[1,63],20:h,22:f,23:d,24:p,25:y,26:g,27:m,30:29,31:6,36:v,37:b,38:_,39:x,42:w,45:i},t(T,[2,19])],defaultActions:{7:[2,40],8:[2,1],9:[2,2],10:[2,3],47:[2,38],48:[2,39],50:[2,42]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},M={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:case 26:return 36;case 1:case 27:return 37;case 2:case 28:return 38;case 3:case 29:return 39;case 4:return this.begin("open_directive"),45;case 5:return this.begin("type_directive"),46;case 6:return this.popState(),this.begin("arg_directive"),34;case 7:return this.popState(),this.popState(),48;case 8:return 47;case 9:case 10:case 12:case 13:case 14:case 15:case 39:case 45:break;case 11:case 59:return 5;case 16:return this.pushState("SCALE"),15;case 17:return 16;case 18:case 33:case 36:this.popState();break;case 19:this.pushState("STATE");break;case 20:case 23:return this.popState(),e.yytext=e.yytext.slice(0,-8).trim(),23;case 21:case 24:return this.popState(),e.yytext=e.yytext.slice(0,-8).trim(),24;case 22:case 25:return this.popState(),e.yytext=e.yytext.slice(0,-10).trim(),25;case 30:this.begin("STATE_STRING");break;case 31:return this.popState(),this.pushState("STATE_ID"),"AS";case 32:case 47:return this.popState(),"ID";case 34:return"STATE_DESCR";case 35:return 17;case 37:return this.popState(),this.pushState("struct"),18;case 38:return this.popState(),19;case 40:return this.begin("NOTE"),27;case 41:return this.popState(),this.pushState("NOTE_ID"),43;case 42:return this.popState(),this.pushState("NOTE_ID"),44;case 43:this.popState(),this.pushState("FLOATING_NOTE");break;case 44:return this.popState(),this.pushState("FLOATING_NOTE_ID"),"AS";case 46:return"NOTE_TEXT";case 48:return this.popState(),this.pushState("NOTE_TEXT"),22;case 49:return this.popState(),e.yytext=e.yytext.substr(2).trim(),29;case 50:return this.popState(),e.yytext=e.yytext.slice(0,-8).trim(),29;case 51:case 52:return 7;case 53:return 14;case 54:return 42;case 55:return 22;case 56:return e.yytext=e.yytext.trim(),12;case 57:return 13;case 58:return 26;case 60:return"INVALID"}},rules:[/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:[\s]+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:scale\s+)/i,/^(?:\d+)/i,/^(?:\s+width\b)/i,/^(?:state\s+)/i,/^(?:.*<>)/i,/^(?:.*<>)/i,/^(?:.*<>)/i,/^(?:.*\[\[fork\]\])/i,/^(?:.*\[\[join\]\])/i,/^(?:.*\[\[choice\]\])/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:["])/i,/^(?:\s*as\s+)/i,/^(?:[^\n\{]*)/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[^\n\s\{]+)/i,/^(?:\n)/i,/^(?:\{)/i,/^(?:\})/i,/^(?:[\n])/i,/^(?:note\s+)/i,/^(?:left of\b)/i,/^(?:right of\b)/i,/^(?:")/i,/^(?:\s*as\s*)/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[^\n]*)/i,/^(?:\s*[^:\n\s\-]+)/i,/^(?:\s*:[^:\n;]+)/i,/^(?:[\s\S]*?end note\b)/i,/^(?:stateDiagram\s+)/i,/^(?:stateDiagram-v2\s+)/i,/^(?:hide empty description\b)/i,/^(?:\[\*\])/i,/^(?:[^:\n\s\-\{]+)/i,/^(?:\s*:[^:\n;]+)/i,/^(?:-->)/i,/^(?:--)/i,/^(?:$)/i,/^(?:.)/i],conditions:{LINE:{rules:[13,14],inclusive:!1},close_directive:{rules:[13,14],inclusive:!1},arg_directive:{rules:[7,8,13,14],inclusive:!1},type_directive:{rules:[6,7,13,14],inclusive:!1},open_directive:{rules:[5,13,14],inclusive:!1},struct:{rules:[13,14,19,26,27,28,29,38,39,40,54,55,56,57,58],inclusive:!1},FLOATING_NOTE_ID:{rules:[47],inclusive:!1},FLOATING_NOTE:{rules:[44,45,46],inclusive:!1},NOTE_TEXT:{rules:[49,50],inclusive:!1},NOTE_ID:{rules:[48],inclusive:!1},NOTE:{rules:[41,42,43],inclusive:!1},SCALE:{rules:[17,18],inclusive:!1},ALIAS:{rules:[],inclusive:!1},STATE_ID:{rules:[32],inclusive:!1},STATE_STRING:{rules:[33,34],inclusive:!1},FORK_STATE:{rules:[],inclusive:!1},STATE:{rules:[13,14,20,21,22,23,24,25,30,31,35,36,37],inclusive:!1},ID:{rules:[13,14],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,9,10,11,12,14,15,16,19,37,40,51,52,53,54,55,56,57,59,60],inclusive:!0}}};function N(){this.yy={}}return A.lexer=M,N.prototype=A,A.Parser=N,new N}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(3069).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},9763:(t,e,n)=>{t=n.nmd(t);var r=function(){var t=function(t,e,n,r){for(n=n||{},r=t.length;r--;n[t[r]]=e);return n},e=[1,2],n=[1,5],r=[6,9,11,17,18,19,21],i=[1,15],a=[1,16],o=[1,17],s=[1,21],c=[4,6,9,11,17,18,19,21],u={trace:function(){},yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,directive:7,line:8,SPACE:9,statement:10,NEWLINE:11,openDirective:12,typeDirective:13,closeDirective:14,":":15,argDirective:16,title:17,section:18,taskName:19,taskData:20,open_directive:21,type_directive:22,arg_directive:23,close_directive:24,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",9:"SPACE",11:"NEWLINE",15:":",17:"title",18:"section",19:"taskName",20:"taskData",21:"open_directive",22:"type_directive",23:"arg_directive",24:"close_directive"},productions_:[0,[3,3],[3,2],[5,0],[5,2],[8,2],[8,1],[8,1],[8,1],[7,4],[7,6],[10,1],[10,1],[10,2],[10,1],[12,1],[13,1],[16,1],[14,1]],performAction:function(t,e,n,r,i,a,o){var s=a.length-1;switch(i){case 1:return a[s-1];case 3:case 7:case 8:this.$=[];break;case 4:a[s-1].push(a[s]),this.$=a[s-1];break;case 5:case 6:this.$=a[s];break;case 11:r.setTitle(a[s].substr(6)),this.$=a[s].substr(6);break;case 12:r.addSection(a[s].substr(8)),this.$=a[s].substr(8);break;case 13:r.addTask(a[s-1],a[s]),this.$="task";break;case 15:r.parseDirective("%%{","open_directive");break;case 16:r.parseDirective(a[s],"type_directive");break;case 17:a[s]=a[s].trim().replace(/'/g,'"'),r.parseDirective(a[s],"arg_directive");break;case 18:r.parseDirective("}%%","close_directive","journey")}},table:[{3:1,4:e,7:3,12:4,21:n},{1:[3]},t(r,[2,3],{5:6}),{3:7,4:e,7:3,12:4,21:n},{13:8,22:[1,9]},{22:[2,15]},{6:[1,10],7:18,8:11,9:[1,12],10:13,11:[1,14],12:4,17:i,18:a,19:o,21:n},{1:[2,2]},{14:19,15:[1,20],24:s},t([15,24],[2,16]),t(r,[2,8],{1:[2,1]}),t(r,[2,4]),{7:18,10:22,12:4,17:i,18:a,19:o,21:n},t(r,[2,6]),t(r,[2,7]),t(r,[2,11]),t(r,[2,12]),{20:[1,23]},t(r,[2,14]),{11:[1,24]},{16:25,23:[1,26]},{11:[2,18]},t(r,[2,5]),t(r,[2,13]),t(c,[2,9]),{14:27,24:s},{24:[2,17]},{11:[1,28]},t(c,[2,10])],defaultActions:{5:[2,15],7:[2,2],21:[2,18],26:[2,17]},parseError:function(t,e){if(!e.recoverable){var n=new Error(t);throw n.hash=e,n}this.trace(t)},parse:function(t){var e=this,n=[0],r=[],i=[null],a=[],o=this.table,s="",c=0,u=0,l=0,h=2,f=1,d=a.slice.call(arguments,1),p=Object.create(this.lexer),y={yy:{}};for(var g in this.yy)Object.prototype.hasOwnProperty.call(this.yy,g)&&(y.yy[g]=this.yy[g]);p.setInput(t,y.yy),y.yy.lexer=p,y.yy.parser=this,void 0===p.yylloc&&(p.yylloc={});var m=p.yylloc;a.push(m);var v=p.options&&p.options.ranges;function b(){var t;return"number"!=typeof(t=r.pop()||p.lex()||f)&&(t instanceof Array&&(t=(r=t).pop()),t=e.symbols_[t]||t),t}"function"==typeof y.yy.parseError?this.parseError=y.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;for(var _,x,w,k,T,E,C,S,A,M={};;){if(w=n[n.length-1],this.defaultActions[w]?k=this.defaultActions[w]:(null==_&&(_=b()),k=o[w]&&o[w][_]),void 0===k||!k.length||!k[0]){var N="";for(E in A=[],o[w])this.terminals_[E]&&E>h&&A.push("'"+this.terminals_[E]+"'");N=p.showPosition?"Parse error on line "+(c+1)+":\n"+p.showPosition()+"\nExpecting "+A.join(", ")+", got '"+(this.terminals_[_]||_)+"'":"Parse error on line "+(c+1)+": Unexpected "+(_==f?"end of input":"'"+(this.terminals_[_]||_)+"'"),this.parseError(N,{text:p.match,token:this.terminals_[_]||_,line:p.yylineno,loc:m,expected:A})}if(k[0]instanceof Array&&k.length>1)throw new Error("Parse Error: multiple actions possible at state: "+w+", token: "+_);switch(k[0]){case 1:n.push(_),i.push(p.yytext),a.push(p.yylloc),n.push(k[1]),_=null,x?(_=x,x=null):(u=p.yyleng,s=p.yytext,c=p.yylineno,m=p.yylloc,l>0&&l--);break;case 2:if(C=this.productions_[k[1]][1],M.$=i[i.length-C],M._$={first_line:a[a.length-(C||1)].first_line,last_line:a[a.length-1].last_line,first_column:a[a.length-(C||1)].first_column,last_column:a[a.length-1].last_column},v&&(M._$.range=[a[a.length-(C||1)].range[0],a[a.length-1].range[1]]),void 0!==(T=this.performAction.apply(M,[s,u,c,y.yy,k[1],i,a].concat(d))))return T;C&&(n=n.slice(0,-1*C*2),i=i.slice(0,-1*C),a=a.slice(0,-1*C)),n.push(this.productions_[k[1]][0]),i.push(M.$),a.push(M._$),S=o[n[n.length-2]][n[n.length-1]],n.push(S);break;case 3:return!0}}return!0}},l={EOF:1,parseError:function(t,e){if(!this.yy.parser)throw new Error(t);this.yy.parser.parseError(t,e)},setInput:function(t,e){return this.yy=e||this.yy||{},this._input=t,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},input:function(){var t=this._input[0];return this.yytext+=t,this.yyleng++,this.offset++,this.match+=t,this.matched+=t,t.match(/(?:\r\n?|\n).*/g)?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),t},unput:function(t){var e=t.length,n=t.split(/(?:\r\n?|\n)/g);this._input=t+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-e),this.offset-=e;var r=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),n.length-1&&(this.yylineno-=n.length-1);var i=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:n?(n.length===r.length?this.yylloc.first_column:0)+r[r.length-n.length].length-n[0].length:this.yylloc.first_column-e},this.options.ranges&&(this.yylloc.range=[i[0],i[0]+this.yyleng-e]),this.yyleng=this.yytext.length,this},more:function(){return this._more=!0,this},reject:function(){return this.options.backtrack_lexer?(this._backtrack=!0,this):this.parseError("Lexical error on line "+(this.yylineno+1)+". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},less:function(t){this.unput(this.match.slice(t))},pastInput:function(){var t=this.matched.substr(0,this.matched.length-this.match.length);return(t.length>20?"...":"")+t.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var t=this.match;return t.length<20&&(t+=this._input.substr(0,20-t.length)),(t.substr(0,20)+(t.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var t=this.pastInput(),e=new Array(t.length+1).join("-");return t+this.upcomingInput()+"\n"+e+"^"},test_match:function(t,e){var n,r,i;if(this.options.backtrack_lexer&&(i={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(i.yylloc.range=this.yylloc.range.slice(0))),(r=t[0].match(/(?:\r\n?|\n).*/g))&&(this.yylineno+=r.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:r?r[r.length-1].length-r[r.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+t[0].length},this.yytext+=t[0],this.match+=t[0],this.matches=t,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(t[0].length),this.matched+=t[0],n=this.performAction.call(this,this.yy,this,e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),n)return n;if(this._backtrack){for(var a in i)this[a]=i[a];return!1}return!1},next:function(){if(this.done)return this.EOF;var t,e,n,r;this._input||(this.done=!0),this._more||(this.yytext="",this.match="");for(var i=this._currentRules(),a=0;ae[0].length)){if(e=n,r=a,this.options.backtrack_lexer){if(!1!==(t=this.test_match(n,i[a])))return t;if(this._backtrack){e=!1;continue}return!1}if(!this.options.flex)break}return e?!1!==(t=this.test_match(e,i[r]))&&t:""===this._input?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){return this.next()||this.lex()},begin:function(t){this.conditionStack.push(t)},popState:function(){return this.conditionStack.length-1>0?this.conditionStack.pop():this.conditionStack[0]},_currentRules:function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},topState:function(t){return(t=this.conditionStack.length-1-Math.abs(t||0))>=0?this.conditionStack[t]:"INITIAL"},pushState:function(t){this.begin(t)},stateStackSize:function(){return this.conditionStack.length},options:{"case-insensitive":!0},performAction:function(t,e,n,r){switch(n){case 0:return this.begin("open_directive"),21;case 1:return this.begin("type_directive"),22;case 2:return this.popState(),this.begin("arg_directive"),15;case 3:return this.popState(),this.popState(),24;case 4:return 23;case 5:case 6:case 8:case 9:break;case 7:return 11;case 10:return 4;case 11:return 17;case 12:return 18;case 13:return 19;case 14:return 20;case 15:return 15;case 16:return 6;case 17:return"INVALID"}},rules:[/^(?:%%\{)/i,/^(?:((?:(?!\}%%)[^:.])*))/i,/^(?::)/i,/^(?:\}%%)/i,/^(?:((?:(?!\}%%).|\n)*))/i,/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:journey\b)/i,/^(?:title\s[^#\n;]+)/i,/^(?:section\s[^#:\n;]+)/i,/^(?:[^#:\n;]+)/i,/^(?::[^#\n;]+)/i,/^(?::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{open_directive:{rules:[1],inclusive:!1},type_directive:{rules:[2,3],inclusive:!1},arg_directive:{rules:[3,4],inclusive:!1},INITIAL:{rules:[0,5,6,7,8,9,10,11,12,13,14,15,16,17],inclusive:!0}}};function h(){this.yy={}}return u.lexer=l,h.prototype=u,u.Parser=h,new h}();e.parser=r,e.Parser=r.Parser,e.parse=function(){return r.parse.apply(r,arguments)},e.main=function(t){t[1]||(console.log("Usage: "+t[0]+" FILE"),process.exit(1));var r=n(9143).readFileSync(n(6470).normalize(t[1]),"utf8");return e.parser.parse(r)},n.c[n.s]===t&&e.main(process.argv.slice(1))},9609:t=>{"use strict";var e=/^(%20|\s)*(javascript|data)/im,n=/[^\x20-\x7E]/gim,r=/^([^:]+):/gm,i=[".","/"];t.exports={sanitizeUrl:function(t){if(!t)return"about:blank";var a,o,s=t.replace(n,"").trim();return function(t){return i.indexOf(t[0])>-1}(s)?s:(o=s.match(r))?(a=o[0],e.test(a)?"about:blank":s):"about:blank"}}},3841:t=>{t.exports=function(t,e){return t.intersect(e)}},7458:(t,e,n)=>{"use strict";n.d(e,{default:()=>fC});var r=n(1941),i=n.n(r),a={debug:1,info:2,warn:3,error:4,fatal:5},o={debug:function(){},info:function(){},warn:function(){},error:function(){},fatal:function(){}},s=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"fatal";isNaN(t)&&(t=t.toLowerCase(),void 0!==a[t]&&(t=a[t])),o.trace=function(){},o.debug=function(){},o.info=function(){},o.warn=function(){},o.error=function(){},o.fatal=function(){},t<=a.fatal&&(o.fatal=console.error?console.error.bind(console,c("FATAL"),"color: orange"):console.log.bind(console,"",c("FATAL"))),t<=a.error&&(o.error=console.error?console.error.bind(console,c("ERROR"),"color: orange"):console.log.bind(console,"",c("ERROR"))),t<=a.warn&&(o.warn=console.warn?console.warn.bind(console,c("WARN"),"color: orange"):console.log.bind(console,"",c("WARN"))),t<=a.info&&(o.info=console.info?console.info.bind(console,c("INFO"),"color: lightblue"):console.log.bind(console,"",c("INFO"))),t<=a.debug&&(o.debug=console.debug?console.debug.bind(console,c("DEBUG"),"color: lightgreen"):console.log.bind(console,"",c("DEBUG")))},c=function(t){var e=i()().format("ss.SSS");return"%c".concat(e," : ").concat(t," : ")};function u(t,e){let n;if(void 0===e)for(const e of t)null!=e&&(n=e)&&(n=e);else{let r=-1;for(let i of t)null!=(i=e(i,++r,t))&&(n=i)&&(n=i)}return n}function l(t,e){let n;if(void 0===e)for(const e of t)null!=e&&(n>e||void 0===n&&e>=e)&&(n=e);else{let r=-1;for(let i of t)null!=(i=e(i,++r,t))&&(n>i||void 0===n&&i>=i)&&(n=i)}return n}function h(t){return t}var f=1e-6;function d(t){return"translate("+t+",0)"}function p(t){return"translate(0,"+t+")"}function y(t){return e=>+t(e)}function g(t,e){return e=Math.max(0,t.bandwidth()-2*e)/2,t.round()&&(e=Math.round(e)),n=>+t(n)+e}function m(){return!this.__axis}function v(t,e){var n=[],r=null,i=null,a=6,o=6,s=3,c="undefined"!=typeof window&&window.devicePixelRatio>1?0:.5,u=1===t||4===t?-1:1,l=4===t||2===t?"x":"y",v=1===t||3===t?d:p;function b(d){var p=null==r?e.ticks?e.ticks.apply(e,n):e.domain():r,b=null==i?e.tickFormat?e.tickFormat.apply(e,n):h:i,_=Math.max(a,0)+s,x=e.range(),w=+x[0]+c,k=+x[x.length-1]+c,T=(e.bandwidth?g:y)(e.copy(),c),E=d.selection?d.selection():d,C=E.selectAll(".domain").data([null]),S=E.selectAll(".tick").data(p,e).order(),A=S.exit(),M=S.enter().append("g").attr("class","tick"),N=S.select("line"),D=S.select("text");C=C.merge(C.enter().insert("path",".tick").attr("class","domain").attr("stroke","currentColor")),S=S.merge(M),N=N.merge(M.append("line").attr("stroke","currentColor").attr(l+"2",u*a)),D=D.merge(M.append("text").attr("fill","currentColor").attr(l,u*_).attr("dy",1===t?"0em":3===t?"0.71em":"0.32em")),d!==E&&(C=C.transition(d),S=S.transition(d),N=N.transition(d),D=D.transition(d),A=A.transition(d).attr("opacity",f).attr("transform",(function(t){return isFinite(t=T(t))?v(t+c):this.getAttribute("transform")})),M.attr("opacity",f).attr("transform",(function(t){var e=this.parentNode.__axis;return v((e&&isFinite(e=e(t))?e:T(t))+c)}))),A.remove(),C.attr("d",4===t||2===t?o?"M"+u*o+","+w+"H"+c+"V"+k+"H"+u*o:"M"+c+","+w+"V"+k:o?"M"+w+","+u*o+"V"+c+"H"+k+"V"+u*o:"M"+w+","+c+"H"+k),S.attr("opacity",1).attr("transform",(function(t){return v(T(t)+c)})),N.attr(l+"2",u*a),D.attr(l,u*_).text(b),E.filter(m).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",2===t?"start":4===t?"end":"middle"),E.each((function(){this.__axis=T}))}return b.scale=function(t){return arguments.length?(e=t,b):e},b.ticks=function(){return n=Array.from(arguments),b},b.tickArguments=function(t){return arguments.length?(n=null==t?[]:Array.from(t),b):n.slice()},b.tickValues=function(t){return arguments.length?(r=null==t?null:Array.from(t),b):r&&r.slice()},b.tickFormat=function(t){return arguments.length?(i=t,b):i},b.tickSize=function(t){return arguments.length?(a=o=+t,b):a},b.tickSizeInner=function(t){return arguments.length?(a=+t,b):a},b.tickSizeOuter=function(t){return arguments.length?(o=+t,b):o},b.tickPadding=function(t){return arguments.length?(s=+t,b):s},b.offset=function(t){return arguments.length?(c=+t,b):c},b}function b(){}function _(t){return null==t?b:function(){return this.querySelector(t)}}function x(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function w(){return[]}function k(t){return null==t?w:function(){return this.querySelectorAll(t)}}function T(t){return function(){return this.matches(t)}}function E(t){return function(e){return e.matches(t)}}var C=Array.prototype.find;function S(){return this.firstElementChild}var A=Array.prototype.filter;function M(){return Array.from(this.children)}function N(t){return new Array(t.length)}function D(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}function O(t){return function(){return t}}function B(t,e,n,r,i,a){for(var o,s=0,c=e.length,u=a.length;se?1:t>=e?0:NaN}D.prototype={constructor:D,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var P="http://www.w3.org/1999/xhtml";const j={svg:"http://www.w3.org/2000/svg",xhtml:P,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function Y(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),j.hasOwnProperty(e)?{space:j[e],local:t}:t}function z(t){return function(){this.removeAttribute(t)}}function U(t){return function(){this.removeAttributeNS(t.space,t.local)}}function q(t,e){return function(){this.setAttribute(t,e)}}function H(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function $(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function W(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function V(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function G(t){return function(){this.style.removeProperty(t)}}function X(t,e,n){return function(){this.style.setProperty(t,e,n)}}function Z(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function Q(t,e){return t.style.getPropertyValue(e)||V(t).getComputedStyle(t,null).getPropertyValue(e)}function K(t){return function(){delete this[t]}}function J(t,e){return function(){this[t]=e}}function tt(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function et(t){return t.trim().split(/^|\s+/)}function nt(t){return t.classList||new rt(t)}function rt(t){this._node=t,this._names=et(t.getAttribute("class")||"")}function it(t,e){for(var n=nt(t),r=-1,i=e.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function Et(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,a=e.length;r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Nt=[null];function Dt(t,e){this._groups=t,this._parents=e}function Ot(){return new Dt([[document.documentElement]],Nt)}Dt.prototype=Ot.prototype={constructor:Dt,select:function(t){"function"!=typeof t&&(t=_(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=x&&(x=_+1);!(b=g[x])&&++x=0;)(r=i[a])&&(o&&4^r.compareDocumentPosition(o)&&o.parentNode.insertBefore(r,o),o=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=F);for(var n=this._groups,r=n.length,i=new Array(r),a=0;a1?this.each((null==e?G:"function"==typeof e?Z:X)(t,e,null==n?"":n)):Q(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?K:"function"==typeof e?tt:J)(t,e)):this.node()[t]},classed:function(t,e){var n=et(t+"");if(arguments.length<2){for(var r=nt(this.node()),i=-1,a=n.length;++i{}};function It(){for(var t,e=0,n=arguments.length,r={};e=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function Pt(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),a=0;a=0&&e._call.call(void 0,t),e=e._next;--qt}()}finally{qt=0,function(){for(var t,e,n=zt,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:zt=e);Ut=t,re(r)}(),Vt=0}}function ne(){var t=Xt.now(),e=t-Wt;e>1e3&&(Gt-=e,Wt=t)}function re(t){qt||(Ht&&(Ht=clearTimeout(Ht)),t-Vt>24?(t<1/0&&(Ht=setTimeout(ee,t-Xt.now()-Gt)),$t&&($t=clearInterval($t))):($t||(Wt=Xt.now(),$t=setInterval(ne,1e3)),qt=1,Zt(ee)))}function ie(t,e,n){var r=new Jt;return e=null==e?0:+e,r.restart((n=>{r.stop(),t(n+e)}),e,n),r}Jt.prototype=te.prototype={constructor:Jt,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?Qt():+n)+(null==e?0:+e),this._next||Ut===this||(Ut?Ut._next=this:zt=this,Ut=this),this._call=t,this._time=n,re()},stop:function(){this._call&&(this._call=null,this._time=1/0,re())}};var ae=Yt("start","end","cancel","interrupt"),oe=[];function se(t,e,n,r,i,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function a(c){var u,l,h,f;if(1!==n.state)return s();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return ie(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function ue(t,e){var n=le(t,e);if(n.state>3)throw new Error("too late; already running");return n}function le(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function he(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var fe,de=180/Math.PI,pe={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function ye(t,e,n,r,i,a){var o,s,c;return(o=Math.sqrt(t*t+e*e))&&(t/=o,e/=o),(c=t*n+e*r)&&(n-=t*c,r-=e*c),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,c/=s),t*r180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(i(n)+"rotate(",null,r)-2,x:he(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(a.rotate,o.rotate,s,c),function(t,e,n,a){t!==e?a.push({i:n.push(i(n)+"skewX(",null,r)-2,x:he(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(a.skewX,o.skewX,s,c),function(t,e,n,r,a,o){if(t!==n||e!==r){var s=a.push(i(a)+"scale(",null,",",null,")");o.push({i:s-4,x:he(t,n)},{i:s-2,x:he(e,r)})}else 1===n&&1===r||a.push(i(a)+"scale("+n+","+r+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,c),a=o=null,function(t){for(var e,n=-1,r=c.length;++n>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?Ue(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?Ue(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=De.exec(t))?new $e(e[1],e[2],e[3],1):(e=Oe.exec(t))?new $e(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Be.exec(t))?Ue(e[1],e[2],e[3],e[4]):(e=Le.exec(t))?Ue(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Ie.exec(t))?Xe(e[1],e[2]/100,e[3]/100,1):(e=Re.exec(t))?Xe(e[1],e[2]/100,e[3]/100,e[4]):Fe.hasOwnProperty(t)?ze(Fe[t]):"transparent"===t?new $e(NaN,NaN,NaN,0):null}function ze(t){return new $e(t>>16&255,t>>8&255,255&t,1)}function Ue(t,e,n,r){return r<=0&&(t=e=n=NaN),new $e(t,e,n,r)}function qe(t){return t instanceof Te||(t=Ye(t)),t?new $e((t=t.rgb()).r,t.g,t.b,t.opacity):new $e}function He(t,e,n,r){return 1===arguments.length?qe(t):new $e(t,e,n,null==r?1:r)}function $e(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function We(){return"#"+Ge(this.r)+Ge(this.g)+Ge(this.b)}function Ve(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function Ge(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function Xe(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new Qe(t,e,n,r)}function Ze(t){if(t instanceof Qe)return new Qe(t.h,t.s,t.l,t.opacity);if(t instanceof Te||(t=Ye(t)),!t)return new Qe;if(t instanceof Qe)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new Qe(o,s,c,t.opacity)}function Qe(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function Ke(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}function Je(t,e,n,r,i){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*r+o*i)/6}we(Te,Ye,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:Pe,formatHex:Pe,formatHsl:function(){return Ze(this).formatHsl()},formatRgb:je,toString:je}),we($e,He,ke(Te,{brighter:function(t){return t=null==t?Ce:Math.pow(Ce,t),new $e(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Ee:Math.pow(Ee,t),new $e(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:We,formatHex:We,formatRgb:Ve,toString:Ve})),we(Qe,(function(t,e,n,r){return 1===arguments.length?Ze(t):new Qe(t,e,n,null==r?1:r)}),ke(Te,{brighter:function(t){return t=null==t?Ce:Math.pow(Ce,t),new Qe(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Ee:Math.pow(Ee,t),new Qe(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new $e(Ke(t>=240?t-240:t+120,i,r),Ke(t,i,r),Ke(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const tn=t=>()=>t;function en(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):tn(isNaN(t)?e:t)}const nn=function t(e){var n=function(t){return 1==(t=+t)?en:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):tn(isNaN(e)?n:e)}}(e);function r(t,e){var r=n((t=He(t)).r,(e=He(e)).r),i=n(t.g,e.g),a=n(t.b,e.b),o=en(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=a(e),t.opacity=o(e),t+""}}return r.gamma=t,r}(1);function rn(t){return function(e){var n,r,i=e.length,a=new Array(i),o=new Array(i),s=new Array(i);for(n=0;n=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=ra&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:he(n,r)})),a=on.lastIndex;return a=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?ce:ue;return function(){var o=a(this,t),s=o.on;s!==r&&(i=(r=s).copy()).on(e,n),o.on=i}}var En=Bt.prototype.constructor;function Cn(t){return function(){this.style.removeProperty(t)}}function Sn(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function An(t,e,n){var r,i;function a(){var a=e.apply(this,arguments);return a!==i&&(r=(i=a)&&Sn(t,a,n)),r}return a._value=e,a}function Mn(t){return function(e){this.textContent=t.call(this,e)}}function Nn(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&Mn(r)),e}return r._value=t,r}var Dn=0;function On(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function Bn(){return++Dn}var Ln=Bt.prototype;On.prototype=function(t){return Bt().transition(t)}.prototype={constructor:On,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=_(t));for(var r=this._groups,i=r.length,a=new Array(i),o=0;o2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete a[i]):o=!1;o&&delete t.__transition}}(this,t)}))},Bt.prototype.transition=function(t){var e,n;t instanceof On?(e=t._id,t=t._name):(e=Bn(),(n=In).time=Qt(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,a=0;a>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?sr(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?sr(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Zn.exec(t))?new lr(e[1],e[2],e[3],1):(e=Qn.exec(t))?new lr(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Kn.exec(t))?sr(e[1],e[2],e[3],e[4]):(e=Jn.exec(t))?sr(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=tr.exec(t))?pr(e[1],e[2]/100,e[3]/100,1):(e=er.exec(t))?pr(e[1],e[2]/100,e[3]/100,e[4]):nr.hasOwnProperty(t)?or(nr[t]):"transparent"===t?new lr(NaN,NaN,NaN,0):null}function or(t){return new lr(t>>16&255,t>>8&255,255&t,1)}function sr(t,e,n,r){return r<=0&&(t=e=n=NaN),new lr(t,e,n,r)}function cr(t){return t instanceof qn||(t=ar(t)),t?new lr((t=t.rgb()).r,t.g,t.b,t.opacity):new lr}function ur(t,e,n,r){return 1===arguments.length?cr(t):new lr(t,e,n,null==r?1:r)}function lr(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function hr(){return"#"+dr(this.r)+dr(this.g)+dr(this.b)}function fr(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function dr(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function pr(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new gr(t,e,n,r)}function yr(t){if(t instanceof gr)return new gr(t.h,t.s,t.l,t.opacity);if(t instanceof qn||(t=ar(t)),!t)return new gr;if(t instanceof gr)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new gr(o,s,c,t.opacity)}function gr(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function mr(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}zn(qn,ar,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:rr,formatHex:rr,formatHsl:function(){return yr(this).formatHsl()},formatRgb:ir,toString:ir}),zn(lr,ur,Un(qn,{brighter:function(t){return t=null==t?$n:Math.pow($n,t),new lr(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Hn:Math.pow(Hn,t),new lr(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:hr,formatHex:hr,formatRgb:fr,toString:fr})),zn(gr,(function(t,e,n,r){return 1===arguments.length?yr(t):new gr(t,e,n,null==r?1:r)}),Un(qn,{brighter:function(t){return t=null==t?$n:Math.pow($n,t),new gr(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Hn:Math.pow(Hn,t),new gr(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new lr(mr(t>=240?t-240:t+120,i,r),mr(t,i,r),mr(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const vr=Math.PI/180,br=180/Math.PI,_r=.96422,xr=.82521,wr=4/29,kr=6/29,Tr=3*kr*kr;function Er(t){if(t instanceof Cr)return new Cr(t.l,t.a,t.b,t.opacity);if(t instanceof Br)return Lr(t);t instanceof lr||(t=cr(t));var e,n,r=Nr(t.r),i=Nr(t.g),a=Nr(t.b),o=Sr((.2225045*r+.7168786*i+.0606169*a)/1);return r===i&&i===a?e=n=o:(e=Sr((.4360747*r+.3850649*i+.1430804*a)/_r),n=Sr((.0139322*r+.0971045*i+.7141733*a)/xr)),new Cr(116*o-16,500*(e-o),200*(o-n),t.opacity)}function Cr(t,e,n,r){this.l=+t,this.a=+e,this.b=+n,this.opacity=+r}function Sr(t){return t>.008856451679035631?Math.pow(t,1/3):t/Tr+wr}function Ar(t){return t>kr?t*t*t:Tr*(t-wr)}function Mr(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Nr(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Dr(t){if(t instanceof Br)return new Br(t.h,t.c,t.l,t.opacity);if(t instanceof Cr||(t=Er(t)),0===t.a&&0===t.b)return new Br(NaN,0()=>t;function Rr(t,e){return function(n){return t+n*e}}function Fr(t,e){var n=e-t;return n?Rr(t,n):Ir(isNaN(t)?e:t)}function Pr(t){return function(e,n){var r=t((e=Or(e)).h,(n=Or(n)).h),i=Fr(e.c,n.c),a=Fr(e.l,n.l),o=Fr(e.opacity,n.opacity);return function(t){return e.h=r(t),e.c=i(t),e.l=a(t),e.opacity=o(t),e+""}}}const jr=Pr((function(t,e){var n=e-t;return n?Rr(t,n>180||n<-180?n-360*Math.round(n/360):n):Ir(isNaN(t)?e:t)}));Pr(Fr);var Yr=Math.sqrt(50),zr=Math.sqrt(10),Ur=Math.sqrt(2);function qr(t,e,n){var r=(e-t)/Math.max(0,n),i=Math.floor(Math.log(r)/Math.LN10),a=r/Math.pow(10,i);return i>=0?(a>=Yr?10:a>=zr?5:a>=Ur?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(a>=Yr?10:a>=zr?5:a>=Ur?2:1)}function Hr(t,e,n){var r=Math.abs(e-t)/Math.max(0,n),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),a=r/i;return a>=Yr?i*=10:a>=zr?i*=5:a>=Ur&&(i*=2),ee?1:t>=e?0:NaN}function Wr(t){let e=t,n=t,r=t;function i(t,e,i=0,a=t.length){if(i>>1;r(t[n],e)<0?i=n+1:a=n}while(it(e)-n,n=$r,r=(e,n)=>$r(t(e),n)),{left:i,center:function(t,n,r=0,a=t.length){const o=i(t,n,r,a-1);return o>r&&e(t[o-1],n)>-e(t[o],n)?o-1:o},right:function(t,e,i=0,a=t.length){if(i>>1;r(t[n],e)<=0?i=n+1:a=n}while(i>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?gi(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?gi(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=ai.exec(t))?new bi(e[1],e[2],e[3],1):(e=oi.exec(t))?new bi(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=si.exec(t))?gi(e[1],e[2],e[3],e[4]):(e=ci.exec(t))?gi(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=ui.exec(t))?ki(e[1],e[2]/100,e[3]/100,1):(e=li.exec(t))?ki(e[1],e[2]/100,e[3]/100,e[4]):hi.hasOwnProperty(t)?yi(hi[t]):"transparent"===t?new bi(NaN,NaN,NaN,0):null}function yi(t){return new bi(t>>16&255,t>>8&255,255&t,1)}function gi(t,e,n,r){return r<=0&&(t=e=n=NaN),new bi(t,e,n,r)}function mi(t){return t instanceof Kr||(t=pi(t)),t?new bi((t=t.rgb()).r,t.g,t.b,t.opacity):new bi}function vi(t,e,n,r){return 1===arguments.length?mi(t):new bi(t,e,n,null==r?1:r)}function bi(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function _i(){return"#"+wi(this.r)+wi(this.g)+wi(this.b)}function xi(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function wi(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function ki(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new Ei(t,e,n,r)}function Ti(t){if(t instanceof Ei)return new Ei(t.h,t.s,t.l,t.opacity);if(t instanceof Kr||(t=pi(t)),!t)return new Ei;if(t instanceof Ei)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new Ei(o,s,c,t.opacity)}function Ei(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function Ci(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}function Si(t,e,n,r,i){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*r+o*i)/6}Zr(Kr,pi,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:fi,formatHex:fi,formatHsl:function(){return Ti(this).formatHsl()},formatRgb:di,toString:di}),Zr(bi,vi,Qr(Kr,{brighter:function(t){return t=null==t?ti:Math.pow(ti,t),new bi(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Jr:Math.pow(Jr,t),new bi(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:_i,formatHex:_i,formatRgb:xi,toString:xi})),Zr(Ei,(function(t,e,n,r){return 1===arguments.length?Ti(t):new Ei(t,e,n,null==r?1:r)}),Qr(Kr,{brighter:function(t){return t=null==t?ti:Math.pow(ti,t),new Ei(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Jr:Math.pow(Jr,t),new Ei(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new bi(Ci(t>=240?t-240:t+120,i,r),Ci(t,i,r),Ci(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const Ai=t=>()=>t;function Mi(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):Ai(isNaN(t)?e:t)}const Ni=function t(e){var n=function(t){return 1==(t=+t)?Mi:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):Ai(isNaN(e)?n:e)}}(e);function r(t,e){var r=n((t=vi(t)).r,(e=vi(e)).r),i=n(t.g,e.g),a=n(t.b,e.b),o=Mi(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=a(e),t.opacity=o(e),t+""}}return r.gamma=t,r}(1);function Di(t){return function(e){var n,r,i=e.length,a=new Array(i),o=new Array(i),s=new Array(i);for(n=0;n=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=ra&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:Li(n,r)})),a=Fi.lastIndex;return ae&&(n=t,t=e,e=n),u=function(n){return Math.max(t,Math.min(e,n))}),r=c>2?Vi:Wi,i=a=null,h}function h(e){return null==e||isNaN(e=+e)?n:(i||(i=r(o.map(t),s,c)))(t(u(e)))}return h.invert=function(n){return u(e((a||(a=r(s,o.map(t),Li)))(n)))},h.domain=function(t){return arguments.length?(o=Array.from(t,Ui),l()):o.slice()},h.range=function(t){return arguments.length?(s=Array.from(t),l()):s.slice()},h.rangeRound=function(t){return s=Array.from(t),c=zi,l()},h.clamp=function(t){return arguments.length?(u=!!t||Hi,l()):u!==Hi},h.interpolate=function(t){return arguments.length?(c=t,l()):c},h.unknown=function(t){return arguments.length?(n=t,h):n},function(n,r){return t=n,e=r,l()}}()(Hi,Hi)}function Zi(t,e){switch(arguments.length){case 0:break;case 1:this.range(t);break;default:this.range(e).domain(t)}return this}var Qi,Ki=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Ji(t){if(!(e=Ki.exec(t)))throw new Error("invalid format: "+t);var e;return new ta({fill:e[1],align:e[2],sign:e[3],symbol:e[4],zero:e[5],width:e[6],comma:e[7],precision:e[8]&&e[8].slice(1),trim:e[9],type:e[10]})}function ta(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function ea(t,e){if((n=(t=e?t.toExponential(e-1):t.toExponential()).indexOf("e"))<0)return null;var n,r=t.slice(0,n);return[r.length>1?r[0]+r.slice(2):r,+t.slice(n+1)]}function na(t){return(t=ea(Math.abs(t)))?t[1]:NaN}function ra(t,e){var n=ea(t,e);if(!n)return t+"";var r=n[0],i=n[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}Ji.prototype=ta.prototype,ta.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};const ia={"%":(t,e)=>(100*t).toFixed(e),b:t=>Math.round(t).toString(2),c:t=>t+"",d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:(t,e)=>t.toExponential(e),f:(t,e)=>t.toFixed(e),g:(t,e)=>t.toPrecision(e),o:t=>Math.round(t).toString(8),p:(t,e)=>ra(100*t,e),r:ra,s:function(t,e){var n=ea(t,e);if(!n)return t+"";var r=n[0],i=n[1],a=i-(Qi=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,o=r.length;return a===o?r:a>o?r+new Array(a-o+1).join("0"):a>0?r.slice(0,a)+"."+r.slice(a):"0."+new Array(1-a).join("0")+ea(t,Math.max(0,e+a-1))[0]},X:t=>Math.round(t).toString(16).toUpperCase(),x:t=>Math.round(t).toString(16)};function aa(t){return t}var oa,sa,ca,ua=Array.prototype.map,la=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function ha(t){var e=t.domain;return t.ticks=function(t){var n=e();return function(t,e,n){var r,i,a,o,s=-1;if(n=+n,(t=+t)==(e=+e)&&n>0)return[t];if((r=e0){let n=Math.round(t/o),r=Math.round(e/o);for(n*oe&&--r,a=new Array(i=r-n+1);++se&&--r,a=new Array(i=r-n+1);++s0;){if((i=qr(c,u,n))===r)return a[o]=c,a[s]=u,e(a);if(i>0)c=Math.floor(c/i)*i,u=Math.ceil(u/i)*i;else{if(!(i<0))break;c=Math.ceil(c*i)/i,u=Math.floor(u*i)/i}r=i}return t},t}function fa(){var t=Xi();return t.copy=function(){return Gi(t,fa())},Zi.apply(t,arguments),ha(t)}oa=function(t){var e,n,r=void 0===t.grouping||void 0===t.thousands?aa:(e=ua.call(t.grouping,Number),n=t.thousands+"",function(t,r){for(var i=t.length,a=[],o=0,s=e[0],c=0;i>0&&s>0&&(c+s+1>r&&(s=Math.max(1,r-c)),a.push(t.substring(i-=s,i+s)),!((c+=s+1)>r));)s=e[o=(o+1)%e.length];return a.reverse().join(n)}),i=void 0===t.currency?"":t.currency[0]+"",a=void 0===t.currency?"":t.currency[1]+"",o=void 0===t.decimal?".":t.decimal+"",s=void 0===t.numerals?aa:function(t){return function(e){return e.replace(/[0-9]/g,(function(e){return t[+e]}))}}(ua.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",u=void 0===t.minus?"−":t.minus+"",l=void 0===t.nan?"NaN":t.nan+"";function h(t){var e=(t=Ji(t)).fill,n=t.align,h=t.sign,f=t.symbol,d=t.zero,p=t.width,y=t.comma,g=t.precision,m=t.trim,v=t.type;"n"===v?(y=!0,v="g"):ia[v]||(void 0===g&&(g=12),m=!0,v="g"),(d||"0"===e&&"="===n)&&(d=!0,e="0",n="=");var b="$"===f?i:"#"===f&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",_="$"===f?a:/[%p]/.test(v)?c:"",x=ia[v],w=/[defgprs%]/.test(v);function k(t){var i,a,c,f=b,k=_;if("c"===v)k=x(t)+k,t="";else{var T=(t=+t)<0||1/t<0;if(t=isNaN(t)?l:x(Math.abs(t),g),m&&(t=function(t){t:for(var e,n=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(e+1):t}(t)),T&&0==+t&&"+"!==h&&(T=!1),f=(T?"("===h?h:u:"-"===h||"("===h?"":h)+f,k=("s"===v?la[8+Qi/3]:"")+k+(T&&"("===h?")":""),w)for(i=-1,a=t.length;++i(c=t.charCodeAt(i))||c>57){k=(46===c?o+t.slice(i+1):t.slice(i))+k,t=t.slice(0,i);break}}y&&!d&&(t=r(t,1/0));var E=f.length+t.length+k.length,C=E>1)+f+t+k+C.slice(E);break;default:t=C+f+t+k}return s(t)}return g=void 0===g?6:/[gprs]/.test(v)?Math.max(1,Math.min(21,g)):Math.max(0,Math.min(20,g)),k.toString=function(){return t+""},k}return{format:h,formatPrefix:function(t,e){var n=h(((t=Ji(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(na(e)/3))),i=Math.pow(10,-r),a=la[8+r/3];return function(t){return n(i*t)+a}}}}({thousands:",",grouping:[3],currency:["$",""]}),sa=oa.format,ca=oa.formatPrefix;class da extends Map{constructor(t,e=ya){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:e}}),null!=t)for(const[e,n]of t)this.set(e,n)}get(t){return super.get(pa(this,t))}has(t){return super.has(pa(this,t))}set(t,e){return super.set(function({_intern:t,_key:e},n){const r=e(n);return t.has(r)?t.get(r):(t.set(r,n),n)}(this,t),e)}delete(t){return super.delete(function({_intern:t,_key:e},n){const r=e(n);return t.has(r)&&(n=t.get(r),t.delete(r)),n}(this,t))}}function pa({_intern:t,_key:e},n){const r=e(n);return t.has(r)?t.get(r):n}function ya(t){return null!==t&&"object"==typeof t?t.valueOf():t}Set;const ga=Symbol("implicit");function ma(){var t=new da,e=[],n=[],r=ga;function i(i){let a=t.get(i);if(void 0===a){if(r!==ga)return r;t.set(i,a=e.push(i)-1)}return n[a%n.length]}return i.domain=function(n){if(!arguments.length)return e.slice();e=[],t=new da;for(const r of n)t.has(r)||t.set(r,e.push(r)-1);return i},i.range=function(t){return arguments.length?(n=Array.from(t),i):n.slice()},i.unknown=function(t){return arguments.length?(r=t,i):r},i.copy=function(){return ma(e,n).unknown(r)},Zi.apply(i,arguments),i}const va=1e3,ba=6e4,_a=36e5,xa=864e5,wa=6048e5,ka=31536e6;var Ta=new Date,Ea=new Date;function Ca(t,e,n,r){function i(e){return t(e=0===arguments.length?new Date:new Date(+e)),e}return i.floor=function(e){return t(e=new Date(+e)),e},i.ceil=function(n){return t(n=new Date(n-1)),e(n,1),t(n),n},i.round=function(t){var e=i(t),n=i.ceil(t);return t-e0))return s;do{s.push(o=new Date(+n)),e(n,a),t(n)}while(o=e)for(;t(e),!n(e);)e.setTime(e-1)}),(function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;e(t,-1),!n(t););else for(;--r>=0;)for(;e(t,1),!n(t););}))},n&&(i.count=function(e,r){return Ta.setTime(+e),Ea.setTime(+r),t(Ta),t(Ea),Math.floor(n(Ta,Ea))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(e){return r(e)%t==0}:function(e){return i.count(0,e)%t==0}):i:null}),i}var Sa=Ca((function(){}),(function(t,e){t.setTime(+t+e)}),(function(t,e){return e-t}));Sa.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?Ca((function(e){e.setTime(Math.floor(e/t)*t)}),(function(e,n){e.setTime(+e+n*t)}),(function(e,n){return(n-e)/t})):Sa:null};const Aa=Sa;Sa.range;var Ma=Ca((function(t){t.setTime(t-t.getMilliseconds())}),(function(t,e){t.setTime(+t+e*va)}),(function(t,e){return(e-t)/va}),(function(t){return t.getUTCSeconds()}));const Na=Ma;Ma.range;var Da=Ca((function(t){t.setTime(t-t.getMilliseconds()-t.getSeconds()*va)}),(function(t,e){t.setTime(+t+e*ba)}),(function(t,e){return(e-t)/ba}),(function(t){return t.getMinutes()}));const Oa=Da;Da.range;var Ba=Ca((function(t){t.setTime(t-t.getMilliseconds()-t.getSeconds()*va-t.getMinutes()*ba)}),(function(t,e){t.setTime(+t+e*_a)}),(function(t,e){return(e-t)/_a}),(function(t){return t.getHours()}));const La=Ba;Ba.range;var Ia=Ca((t=>t.setHours(0,0,0,0)),((t,e)=>t.setDate(t.getDate()+e)),((t,e)=>(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*ba)/xa),(t=>t.getDate()-1));const Ra=Ia;function Fa(t){return Ca((function(e){e.setDate(e.getDate()-(e.getDay()+7-t)%7),e.setHours(0,0,0,0)}),(function(t,e){t.setDate(t.getDate()+7*e)}),(function(t,e){return(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*ba)/wa}))}Ia.range;var Pa=Fa(0),ja=Fa(1),Ya=Fa(2),za=Fa(3),Ua=Fa(4),qa=Fa(5),Ha=Fa(6),$a=(Pa.range,ja.range,Ya.range,za.range,Ua.range,qa.range,Ha.range,Ca((function(t){t.setDate(1),t.setHours(0,0,0,0)}),(function(t,e){t.setMonth(t.getMonth()+e)}),(function(t,e){return e.getMonth()-t.getMonth()+12*(e.getFullYear()-t.getFullYear())}),(function(t){return t.getMonth()})));const Wa=$a;$a.range;var Va=Ca((function(t){t.setMonth(0,1),t.setHours(0,0,0,0)}),(function(t,e){t.setFullYear(t.getFullYear()+e)}),(function(t,e){return e.getFullYear()-t.getFullYear()}),(function(t){return t.getFullYear()}));Va.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Ca((function(e){e.setFullYear(Math.floor(e.getFullYear()/t)*t),e.setMonth(0,1),e.setHours(0,0,0,0)}),(function(e,n){e.setFullYear(e.getFullYear()+n*t)})):null};const Ga=Va;Va.range;var Xa=Ca((function(t){t.setUTCSeconds(0,0)}),(function(t,e){t.setTime(+t+e*ba)}),(function(t,e){return(e-t)/ba}),(function(t){return t.getUTCMinutes()}));const Za=Xa;Xa.range;var Qa=Ca((function(t){t.setUTCMinutes(0,0,0)}),(function(t,e){t.setTime(+t+e*_a)}),(function(t,e){return(e-t)/_a}),(function(t){return t.getUTCHours()}));const Ka=Qa;Qa.range;var Ja=Ca((function(t){t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+e)}),(function(t,e){return(e-t)/xa}),(function(t){return t.getUTCDate()-1}));const to=Ja;function eo(t){return Ca((function(e){e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7),e.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+7*e)}),(function(t,e){return(e-t)/wa}))}Ja.range;var no=eo(0),ro=eo(1),io=eo(2),ao=eo(3),oo=eo(4),so=eo(5),co=eo(6),uo=(no.range,ro.range,io.range,ao.range,oo.range,so.range,co.range,Ca((function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCMonth(t.getUTCMonth()+e)}),(function(t,e){return e.getUTCMonth()-t.getUTCMonth()+12*(e.getUTCFullYear()-t.getUTCFullYear())}),(function(t){return t.getUTCMonth()})));const lo=uo;uo.range;var ho=Ca((function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCFullYear(t.getUTCFullYear()+e)}),(function(t,e){return e.getUTCFullYear()-t.getUTCFullYear()}),(function(t){return t.getUTCFullYear()}));ho.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Ca((function(e){e.setUTCFullYear(Math.floor(e.getUTCFullYear()/t)*t),e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)}),(function(e,n){e.setUTCFullYear(e.getUTCFullYear()+n*t)})):null};const fo=ho;function po(t,e,n,r,i,a){const o=[[Na,1,va],[Na,5,5e3],[Na,15,15e3],[Na,30,3e4],[a,1,ba],[a,5,3e5],[a,15,9e5],[a,30,18e5],[i,1,_a],[i,3,108e5],[i,6,216e5],[i,12,432e5],[r,1,xa],[r,2,1728e5],[n,1,wa],[e,1,2592e6],[e,3,7776e6],[t,1,ka]];function s(e,n,r){const i=Math.abs(n-e)/r,a=Wr((([,,t])=>t)).right(o,i);if(a===o.length)return t.every(Hr(e/ka,n/ka,r));if(0===a)return Aa.every(Math.max(Hr(e,n,r),1));const[s,c]=o[i/o[a-1][2][t.toLowerCase(),e])))}function Oo(t,e,n){var r=Eo.exec(e.slice(n,n+1));return r?(t.w=+r[0],n+r[0].length):-1}function Bo(t,e,n){var r=Eo.exec(e.slice(n,n+1));return r?(t.u=+r[0],n+r[0].length):-1}function Lo(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.U=+r[0],n+r[0].length):-1}function Io(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.V=+r[0],n+r[0].length):-1}function Ro(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.W=+r[0],n+r[0].length):-1}function Fo(t,e,n){var r=Eo.exec(e.slice(n,n+4));return r?(t.y=+r[0],n+r[0].length):-1}function Po(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),n+r[0].length):-1}function jo(t,e,n){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(e.slice(n,n+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function Yo(t,e,n){var r=Eo.exec(e.slice(n,n+1));return r?(t.q=3*r[0]-3,n+r[0].length):-1}function zo(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.m=r[0]-1,n+r[0].length):-1}function Uo(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.d=+r[0],n+r[0].length):-1}function qo(t,e,n){var r=Eo.exec(e.slice(n,n+3));return r?(t.m=0,t.d=+r[0],n+r[0].length):-1}function Ho(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.H=+r[0],n+r[0].length):-1}function $o(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.M=+r[0],n+r[0].length):-1}function Wo(t,e,n){var r=Eo.exec(e.slice(n,n+2));return r?(t.S=+r[0],n+r[0].length):-1}function Vo(t,e,n){var r=Eo.exec(e.slice(n,n+3));return r?(t.L=+r[0],n+r[0].length):-1}function Go(t,e,n){var r=Eo.exec(e.slice(n,n+6));return r?(t.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function Xo(t,e,n){var r=Co.exec(e.slice(n,n+1));return r?n+r[0].length:-1}function Zo(t,e,n){var r=Eo.exec(e.slice(n));return r?(t.Q=+r[0],n+r[0].length):-1}function Qo(t,e,n){var r=Eo.exec(e.slice(n));return r?(t.s=+r[0],n+r[0].length):-1}function Ko(t,e){return Ao(t.getDate(),e,2)}function Jo(t,e){return Ao(t.getHours(),e,2)}function ts(t,e){return Ao(t.getHours()%12||12,e,2)}function es(t,e){return Ao(1+Ra.count(Ga(t),t),e,3)}function ns(t,e){return Ao(t.getMilliseconds(),e,3)}function rs(t,e){return ns(t,e)+"000"}function is(t,e){return Ao(t.getMonth()+1,e,2)}function as(t,e){return Ao(t.getMinutes(),e,2)}function os(t,e){return Ao(t.getSeconds(),e,2)}function ss(t){var e=t.getDay();return 0===e?7:e}function cs(t,e){return Ao(Pa.count(Ga(t)-1,t),e,2)}function us(t){var e=t.getDay();return e>=4||0===e?Ua(t):Ua.ceil(t)}function ls(t,e){return t=us(t),Ao(Ua.count(Ga(t),t)+(4===Ga(t).getDay()),e,2)}function hs(t){return t.getDay()}function fs(t,e){return Ao(ja.count(Ga(t)-1,t),e,2)}function ds(t,e){return Ao(t.getFullYear()%100,e,2)}function ps(t,e){return Ao((t=us(t)).getFullYear()%100,e,2)}function ys(t,e){return Ao(t.getFullYear()%1e4,e,4)}function gs(t,e){var n=t.getDay();return Ao((t=n>=4||0===n?Ua(t):Ua.ceil(t)).getFullYear()%1e4,e,4)}function ms(t){var e=t.getTimezoneOffset();return(e>0?"-":(e*=-1,"+"))+Ao(e/60|0,"0",2)+Ao(e%60,"0",2)}function vs(t,e){return Ao(t.getUTCDate(),e,2)}function bs(t,e){return Ao(t.getUTCHours(),e,2)}function _s(t,e){return Ao(t.getUTCHours()%12||12,e,2)}function xs(t,e){return Ao(1+to.count(fo(t),t),e,3)}function ws(t,e){return Ao(t.getUTCMilliseconds(),e,3)}function ks(t,e){return ws(t,e)+"000"}function Ts(t,e){return Ao(t.getUTCMonth()+1,e,2)}function Es(t,e){return Ao(t.getUTCMinutes(),e,2)}function Cs(t,e){return Ao(t.getUTCSeconds(),e,2)}function Ss(t){var e=t.getUTCDay();return 0===e?7:e}function As(t,e){return Ao(no.count(fo(t)-1,t),e,2)}function Ms(t){var e=t.getUTCDay();return e>=4||0===e?oo(t):oo.ceil(t)}function Ns(t,e){return t=Ms(t),Ao(oo.count(fo(t),t)+(4===fo(t).getUTCDay()),e,2)}function Ds(t){return t.getUTCDay()}function Os(t,e){return Ao(ro.count(fo(t)-1,t),e,2)}function Bs(t,e){return Ao(t.getUTCFullYear()%100,e,2)}function Ls(t,e){return Ao((t=Ms(t)).getUTCFullYear()%100,e,2)}function Is(t,e){return Ao(t.getUTCFullYear()%1e4,e,4)}function Rs(t,e){var n=t.getUTCDay();return Ao((t=n>=4||0===n?oo(t):oo.ceil(t)).getUTCFullYear()%1e4,e,4)}function Fs(){return"+0000"}function Ps(){return"%"}function js(t){return+t}function Ys(t){return Math.floor(+t/1e3)}function zs(t){return new Date(t)}function Us(t){return t instanceof Date?+t:+new Date(+t)}function qs(t,e,n,r,i,a,o,s,c,u){var l=Xi(),h=l.invert,f=l.domain,d=u(".%L"),p=u(":%S"),y=u("%I:%M"),g=u("%I %p"),m=u("%a %d"),v=u("%b %d"),b=u("%B"),_=u("%Y");function x(t){return(c(t)=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:js,s:Ys,S:os,u:ss,U:cs,V:ls,w:hs,W:fs,x:null,X:null,y:ds,Y:ys,Z:ms,"%":Ps},_={a:function(t){return o[t.getUTCDay()]},A:function(t){return a[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:vs,e:vs,f:ks,g:Ls,G:Rs,H:bs,I:_s,j:xs,L:ws,m:Ts,M:Es,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:js,s:Ys,S:Cs,u:Ss,U:As,V:Ns,w:Ds,W:Os,x:null,X:null,y:Bs,Y:Is,Z:Fs,"%":Ps},x={a:function(t,e,n){var r=d.exec(e.slice(n));return r?(t.w=p.get(r[0].toLowerCase()),n+r[0].length):-1},A:function(t,e,n){var r=h.exec(e.slice(n));return r?(t.w=f.get(r[0].toLowerCase()),n+r[0].length):-1},b:function(t,e,n){var r=m.exec(e.slice(n));return r?(t.m=v.get(r[0].toLowerCase()),n+r[0].length):-1},B:function(t,e,n){var r=y.exec(e.slice(n));return r?(t.m=g.get(r[0].toLowerCase()),n+r[0].length):-1},c:function(t,n,r){return T(t,e,n,r)},d:Uo,e:Uo,f:Go,g:Po,G:Fo,H:Ho,I:Ho,j:qo,L:Vo,m:zo,M:$o,p:function(t,e,n){var r=u.exec(e.slice(n));return r?(t.p=l.get(r[0].toLowerCase()),n+r[0].length):-1},q:Yo,Q:Zo,s:Qo,S:Wo,u:Bo,U:Lo,V:Io,w:Oo,W:Ro,x:function(t,e,r){return T(t,n,e,r)},X:function(t,e,n){return T(t,r,e,n)},y:Po,Y:Fo,Z:jo,"%":Xo};function w(t,e){return function(n){var r,i,a,o=[],s=-1,c=0,u=t.length;for(n instanceof Date||(n=new Date(+n));++s53)return null;"w"in a||(a.w=1),"Z"in a?(i=(r=_o(xo(a.y,0,1))).getUTCDay(),r=i>4||0===i?ro.ceil(r):ro(r),r=to.offset(r,7*(a.V-1)),a.y=r.getUTCFullYear(),a.m=r.getUTCMonth(),a.d=r.getUTCDate()+(a.w+6)%7):(i=(r=bo(xo(a.y,0,1))).getDay(),r=i>4||0===i?ja.ceil(r):ja(r),r=Ra.offset(r,7*(a.V-1)),a.y=r.getFullYear(),a.m=r.getMonth(),a.d=r.getDate()+(a.w+6)%7)}else("W"in a||"U"in a)&&("w"in a||(a.w="u"in a?a.u%7:"W"in a?1:0),i="Z"in a?_o(xo(a.y,0,1)).getUTCDay():bo(xo(a.y,0,1)).getDay(),a.m=0,a.d="W"in a?(a.w+6)%7+7*a.W-(i+5)%7:a.w+7*a.U-(i+6)%7);return"Z"in a?(a.H+=a.Z/100|0,a.M+=a.Z%100,_o(a)):bo(a)}}function T(t,e,n,r){for(var i,a,o=0,s=e.length,c=n.length;o=c)return-1;if(37===(i=e.charCodeAt(o++))){if(i=e.charAt(o++),!(a=x[i in To?e.charAt(o++):i])||(r=a(t,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}return b.x=w(n,b),b.X=w(r,b),b.c=w(e,b),_.x=w(n,_),_.X=w(r,_),_.c=w(e,_),{format:function(t){var e=w(t+="",b);return e.toString=function(){return t},e},parse:function(t){var e=k(t+="",!1);return e.toString=function(){return t},e},utcFormat:function(t){var e=w(t+="",_);return e.toString=function(){return t},e},utcParse:function(t){var e=k(t+="",!0);return e.toString=function(){return t},e}}}({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}),ko=wo.format,wo.parse,wo.utcFormat,wo.utcParse;var Qs=Array.prototype.find;function Ks(){return this.firstElementChild}var Js=Array.prototype.filter;function tc(){return Array.from(this.children)}function ec(t){return new Array(t.length)}function nc(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}function rc(t){return function(){return t}}function ic(t,e,n,r,i,a){for(var o,s=0,c=e.length,u=a.length;se?1:t>=e?0:NaN}nc.prototype={constructor:nc,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var uc="http://www.w3.org/1999/xhtml";const lc={svg:"http://www.w3.org/2000/svg",xhtml:uc,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function hc(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),lc.hasOwnProperty(e)?{space:lc[e],local:t}:t}function fc(t){return function(){this.removeAttribute(t)}}function dc(t){return function(){this.removeAttributeNS(t.space,t.local)}}function pc(t,e){return function(){this.setAttribute(t,e)}}function yc(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function gc(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function mc(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function vc(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function bc(t){return function(){this.style.removeProperty(t)}}function _c(t,e,n){return function(){this.style.setProperty(t,e,n)}}function xc(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function wc(t,e){return t.style.getPropertyValue(e)||vc(t).getComputedStyle(t,null).getPropertyValue(e)}function kc(t){return function(){delete this[t]}}function Tc(t,e){return function(){this[t]=e}}function Ec(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function Cc(t){return t.trim().split(/^|\s+/)}function Sc(t){return t.classList||new Ac(t)}function Ac(t){this._node=t,this._names=Cc(t.getAttribute("class")||"")}function Mc(t,e){for(var n=Sc(t),r=-1,i=e.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function Zc(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,a=e.length;r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var eu=[null];function nu(t,e){this._groups=t,this._parents=e}function ru(){return new nu([[document.documentElement]],eu)}nu.prototype=ru.prototype={constructor:nu,select:function(t){"function"!=typeof t&&(t=$s(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=x&&(x=_+1);!(b=g[x])&&++x=0;)(r=i[a])&&(o&&4^r.compareDocumentPosition(o)&&o.parentNode.insertBefore(r,o),o=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=cc);for(var n=this._groups,r=n.length,i=new Array(r),a=0;a1?this.each((null==e?bc:"function"==typeof e?xc:_c)(t,e,null==n?"":n)):wc(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?kc:"function"==typeof e?Ec:Tc)(t,e)):this.node()[t]},classed:function(t,e){var n=Cc(t+"");if(arguments.length<2){for(var r=Sc(this.node()),i=-1,a=n.length;++iuu)if(Math.abs(l*s-c*u)>uu&&i){var f=n-a,d=r-o,p=s*s+c*c,y=f*f+d*d,g=Math.sqrt(p),m=Math.sqrt(h),v=i*Math.tan((su-Math.acos((p+h-y)/(2*g*m)))/2),b=v/m,_=v/g;Math.abs(b-1)>uu&&(this._+="L"+(t+b*u)+","+(e+b*l)),this._+="A"+i+","+i+",0,0,"+ +(l*f>u*d)+","+(this._x1=t+_*s)+","+(this._y1=e+_*c)}else this._+="L"+(this._x1=t)+","+(this._y1=e)},arc:function(t,e,n,r,i,a){t=+t,e=+e,a=!!a;var o=(n=+n)*Math.cos(r),s=n*Math.sin(r),c=t+o,u=e+s,l=1^a,h=a?r-i:i-r;if(n<0)throw new Error("negative radius: "+n);null===this._x1?this._+="M"+c+","+u:(Math.abs(this._x1-c)>uu||Math.abs(this._y1-u)>uu)&&(this._+="L"+c+","+u),n&&(h<0&&(h=h%cu+cu),h>lu?this._+="A"+n+","+n+",0,1,"+l+","+(t-o)+","+(e-s)+"A"+n+","+n+",0,1,"+l+","+(this._x1=c)+","+(this._y1=u):h>uu&&(this._+="A"+n+","+n+",0,"+ +(h>=su)+","+l+","+(this._x1=t+n*Math.cos(i))+","+(this._y1=e+n*Math.sin(i))))},rect:function(t,e,n,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+e)+"h"+ +n+"v"+ +r+"h"+-n+"Z"},toString:function(){return this._}};const du=fu;function pu(t){return function(){return t}}var yu=Math.abs,gu=Math.atan2,mu=Math.cos,vu=Math.max,bu=Math.min,_u=Math.sin,xu=Math.sqrt,wu=1e-12,ku=Math.PI,Tu=ku/2,Eu=2*ku;function Cu(t){return t>1?0:t<-1?ku:Math.acos(t)}function Su(t){return t>=1?Tu:t<=-1?-Tu:Math.asin(t)}function Au(t){return t.innerRadius}function Mu(t){return t.outerRadius}function Nu(t){return t.startAngle}function Du(t){return t.endAngle}function Ou(t){return t&&t.padAngle}function Bu(t,e,n,r,i,a,o,s){var c=n-t,u=r-e,l=o-i,h=s-a,f=h*c-l*u;if(!(f*fN*N+D*D&&(T=C,E=S),{cx:T,cy:E,x01:-l,y01:-h,x11:T*(i/x-1),y11:E*(i/x-1)}}function Iu(){var t=Au,e=Mu,n=pu(0),r=null,i=Nu,a=Du,o=Ou,s=null;function c(){var c,u,l=+t.apply(this,arguments),h=+e.apply(this,arguments),f=i.apply(this,arguments)-Tu,d=a.apply(this,arguments)-Tu,p=yu(d-f),y=d>f;if(s||(s=c=du()),hwu)if(p>Eu-wu)s.moveTo(h*mu(f),h*_u(f)),s.arc(0,0,h,f,d,!y),l>wu&&(s.moveTo(l*mu(d),l*_u(d)),s.arc(0,0,l,d,f,y));else{var g,m,v=f,b=d,_=f,x=d,w=p,k=p,T=o.apply(this,arguments)/2,E=T>wu&&(r?+r.apply(this,arguments):xu(l*l+h*h)),C=bu(yu(h-l)/2,+n.apply(this,arguments)),S=C,A=C;if(E>wu){var M=Su(E/l*_u(T)),N=Su(E/h*_u(T));(w-=2*M)>wu?(_+=M*=y?1:-1,x-=M):(w=0,_=x=(f+d)/2),(k-=2*N)>wu?(v+=N*=y?1:-1,b-=N):(k=0,v=b=(f+d)/2)}var D=h*mu(v),O=h*_u(v),B=l*mu(x),L=l*_u(x);if(C>wu){var I,R=h*mu(b),F=h*_u(b),P=l*mu(_),j=l*_u(_);if(pwu?A>wu?(g=Lu(P,j,D,O,h,A,y),m=Lu(R,F,B,L,h,A,y),s.moveTo(g.cx+g.x01,g.cy+g.y01),Awu&&w>wu?S>wu?(g=Lu(B,L,R,F,l,-S,y),m=Lu(D,O,P,j,l,-S,y),s.lineTo(g.cx+g.x01,g.cy+g.y01),St?1:e>=t?0:NaN}function qu(t){return t}function Hu(){}function $u(t,e,n){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+e)/6,(t._y0+4*t._y1+n)/6)}function Wu(t){this._context=t}function Vu(t){return new Wu(t)}function Gu(t){this._context=t}function Xu(t){this._context=t}function Zu(t){this._context=t}function Qu(t){return t<0?-1:1}function Ku(t,e,n){var r=t._x1-t._x0,i=e-t._x1,a=(t._y1-t._y0)/(r||i<0&&-0),o=(n-t._y1)/(i||r<0&&-0),s=(a*i+o*r)/(r+i);return(Qu(a)+Qu(o))*Math.min(Math.abs(a),Math.abs(o),.5*Math.abs(s))||0}function Ju(t,e){var n=t._x1-t._x0;return n?(3*(t._y1-t._y0)/n-e)/2:e}function tl(t,e,n){var r=t._x0,i=t._y0,a=t._x1,o=t._y1,s=(a-r)/3;t._context.bezierCurveTo(r+s,i+s*e,a-s,o-s*n,a,o)}function el(t){this._context=t}function nl(t){this._context=new rl(t)}function rl(t){this._context=t}function il(t){this._context=t}function al(t){var e,n,r=t.length-1,i=new Array(r),a=new Array(r),o=new Array(r);for(i[0]=0,a[0]=2,o[0]=t[0]+2*t[1],e=1;e=0;--e)i[e]=(o[e]-i[e+1])/a[e];for(a[r-1]=(t[r]+i[r-1])/2,e=0;e=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,e),this._context.lineTo(t,e);else{var n=this._x*(1-this._t)+t*this._t;this._context.lineTo(n,this._y),this._context.lineTo(n,e)}}this._x=t,this._y=e}};var sl=new Date,cl=new Date;function ul(t,e,n,r){function i(e){return t(e=0===arguments.length?new Date:new Date(+e)),e}return i.floor=function(e){return t(e=new Date(+e)),e},i.ceil=function(n){return t(n=new Date(n-1)),e(n,1),t(n),n},i.round=function(t){var e=i(t),n=i.ceil(t);return t-e0))return s;do{s.push(o=new Date(+n)),e(n,a),t(n)}while(o=e)for(;t(e),!n(e);)e.setTime(e-1)}),(function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;e(t,-1),!n(t););else for(;--r>=0;)for(;e(t,1),!n(t););}))},n&&(i.count=function(e,r){return sl.setTime(+e),cl.setTime(+r),t(sl),t(cl),Math.floor(n(sl,cl))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(e){return r(e)%t==0}:function(e){return i.count(0,e)%t==0}):i:null}),i}const ll=864e5,hl=6048e5;function fl(t){return ul((function(e){e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7),e.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+7*e)}),(function(t,e){return(e-t)/hl}))}var dl=fl(0),pl=fl(1),yl=fl(2),gl=fl(3),ml=fl(4),vl=fl(5),bl=fl(6),_l=(dl.range,pl.range,yl.range,gl.range,ml.range,vl.range,bl.range,ul((function(t){t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+e)}),(function(t,e){return(e-t)/ll}),(function(t){return t.getUTCDate()-1})));const xl=_l;function wl(t){return ul((function(e){e.setDate(e.getDate()-(e.getDay()+7-t)%7),e.setHours(0,0,0,0)}),(function(t,e){t.setDate(t.getDate()+7*e)}),(function(t,e){return(e-t-6e4*(e.getTimezoneOffset()-t.getTimezoneOffset()))/hl}))}_l.range;var kl=wl(0),Tl=wl(1),El=wl(2),Cl=wl(3),Sl=wl(4),Al=wl(5),Ml=wl(6),Nl=(kl.range,Tl.range,El.range,Cl.range,Sl.range,Al.range,Ml.range,ul((t=>t.setHours(0,0,0,0)),((t,e)=>t.setDate(t.getDate()+e)),((t,e)=>(e-t-6e4*(e.getTimezoneOffset()-t.getTimezoneOffset()))/ll),(t=>t.getDate()-1)));const Dl=Nl;Nl.range;var Ol=ul((function(t){t.setMonth(0,1),t.setHours(0,0,0,0)}),(function(t,e){t.setFullYear(t.getFullYear()+e)}),(function(t,e){return e.getFullYear()-t.getFullYear()}),(function(t){return t.getFullYear()}));Ol.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ul((function(e){e.setFullYear(Math.floor(e.getFullYear()/t)*t),e.setMonth(0,1),e.setHours(0,0,0,0)}),(function(e,n){e.setFullYear(e.getFullYear()+n*t)})):null};const Bl=Ol;Ol.range;var Ll=ul((function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCFullYear(t.getUTCFullYear()+e)}),(function(t,e){return e.getUTCFullYear()-t.getUTCFullYear()}),(function(t){return t.getUTCFullYear()}));Ll.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ul((function(e){e.setUTCFullYear(Math.floor(e.getUTCFullYear()/t)*t),e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)}),(function(e,n){e.setUTCFullYear(e.getUTCFullYear()+n*t)})):null};const Il=Ll;function Rl(t){if(0<=t.y&&t.y<100){var e=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return e.setFullYear(t.y),e}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Fl(t){if(0<=t.y&&t.y<100){var e=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return e.setUTCFullYear(t.y),e}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function Pl(t,e,n){return{y:t,m:e,d:n,H:0,M:0,S:0,L:0}}Ll.range;var jl,Yl,zl={"-":"",_:" ",0:"0"},Ul=/^\s*\d+/,ql=/^%/,Hl=/[\\^$*+?|[\]().{}]/g;function $l(t,e,n){var r=t<0?"-":"",i=(r?-t:t)+"",a=i.length;return r+(a[t.toLowerCase(),e])))}function Xl(t,e,n){var r=Ul.exec(e.slice(n,n+1));return r?(t.w=+r[0],n+r[0].length):-1}function Zl(t,e,n){var r=Ul.exec(e.slice(n,n+1));return r?(t.u=+r[0],n+r[0].length):-1}function Ql(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.U=+r[0],n+r[0].length):-1}function Kl(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.V=+r[0],n+r[0].length):-1}function Jl(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.W=+r[0],n+r[0].length):-1}function th(t,e,n){var r=Ul.exec(e.slice(n,n+4));return r?(t.y=+r[0],n+r[0].length):-1}function eh(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),n+r[0].length):-1}function nh(t,e,n){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(e.slice(n,n+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function rh(t,e,n){var r=Ul.exec(e.slice(n,n+1));return r?(t.q=3*r[0]-3,n+r[0].length):-1}function ih(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.m=r[0]-1,n+r[0].length):-1}function ah(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.d=+r[0],n+r[0].length):-1}function oh(t,e,n){var r=Ul.exec(e.slice(n,n+3));return r?(t.m=0,t.d=+r[0],n+r[0].length):-1}function sh(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.H=+r[0],n+r[0].length):-1}function ch(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.M=+r[0],n+r[0].length):-1}function uh(t,e,n){var r=Ul.exec(e.slice(n,n+2));return r?(t.S=+r[0],n+r[0].length):-1}function lh(t,e,n){var r=Ul.exec(e.slice(n,n+3));return r?(t.L=+r[0],n+r[0].length):-1}function hh(t,e,n){var r=Ul.exec(e.slice(n,n+6));return r?(t.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function fh(t,e,n){var r=ql.exec(e.slice(n,n+1));return r?n+r[0].length:-1}function dh(t,e,n){var r=Ul.exec(e.slice(n));return r?(t.Q=+r[0],n+r[0].length):-1}function ph(t,e,n){var r=Ul.exec(e.slice(n));return r?(t.s=+r[0],n+r[0].length):-1}function yh(t,e){return $l(t.getDate(),e,2)}function gh(t,e){return $l(t.getHours(),e,2)}function mh(t,e){return $l(t.getHours()%12||12,e,2)}function vh(t,e){return $l(1+Dl.count(Bl(t),t),e,3)}function bh(t,e){return $l(t.getMilliseconds(),e,3)}function _h(t,e){return bh(t,e)+"000"}function xh(t,e){return $l(t.getMonth()+1,e,2)}function wh(t,e){return $l(t.getMinutes(),e,2)}function kh(t,e){return $l(t.getSeconds(),e,2)}function Th(t){var e=t.getDay();return 0===e?7:e}function Eh(t,e){return $l(kl.count(Bl(t)-1,t),e,2)}function Ch(t){var e=t.getDay();return e>=4||0===e?Sl(t):Sl.ceil(t)}function Sh(t,e){return t=Ch(t),$l(Sl.count(Bl(t),t)+(4===Bl(t).getDay()),e,2)}function Ah(t){return t.getDay()}function Mh(t,e){return $l(Tl.count(Bl(t)-1,t),e,2)}function Nh(t,e){return $l(t.getFullYear()%100,e,2)}function Dh(t,e){return $l((t=Ch(t)).getFullYear()%100,e,2)}function Oh(t,e){return $l(t.getFullYear()%1e4,e,4)}function Bh(t,e){var n=t.getDay();return $l((t=n>=4||0===n?Sl(t):Sl.ceil(t)).getFullYear()%1e4,e,4)}function Lh(t){var e=t.getTimezoneOffset();return(e>0?"-":(e*=-1,"+"))+$l(e/60|0,"0",2)+$l(e%60,"0",2)}function Ih(t,e){return $l(t.getUTCDate(),e,2)}function Rh(t,e){return $l(t.getUTCHours(),e,2)}function Fh(t,e){return $l(t.getUTCHours()%12||12,e,2)}function Ph(t,e){return $l(1+xl.count(Il(t),t),e,3)}function jh(t,e){return $l(t.getUTCMilliseconds(),e,3)}function Yh(t,e){return jh(t,e)+"000"}function zh(t,e){return $l(t.getUTCMonth()+1,e,2)}function Uh(t,e){return $l(t.getUTCMinutes(),e,2)}function qh(t,e){return $l(t.getUTCSeconds(),e,2)}function Hh(t){var e=t.getUTCDay();return 0===e?7:e}function $h(t,e){return $l(dl.count(Il(t)-1,t),e,2)}function Wh(t){var e=t.getUTCDay();return e>=4||0===e?ml(t):ml.ceil(t)}function Vh(t,e){return t=Wh(t),$l(ml.count(Il(t),t)+(4===Il(t).getUTCDay()),e,2)}function Gh(t){return t.getUTCDay()}function Xh(t,e){return $l(pl.count(Il(t)-1,t),e,2)}function Zh(t,e){return $l(t.getUTCFullYear()%100,e,2)}function Qh(t,e){return $l((t=Wh(t)).getUTCFullYear()%100,e,2)}function Kh(t,e){return $l(t.getUTCFullYear()%1e4,e,4)}function Jh(t,e){var n=t.getUTCDay();return $l((t=n>=4||0===n?ml(t):ml.ceil(t)).getUTCFullYear()%1e4,e,4)}function tf(){return"+0000"}function ef(){return"%"}function nf(t){return+t}function rf(t){return Math.floor(+t/1e3)}jl=function(t){var e=t.dateTime,n=t.date,r=t.time,i=t.periods,a=t.days,o=t.shortDays,s=t.months,c=t.shortMonths,u=Vl(i),l=Gl(i),h=Vl(a),f=Gl(a),d=Vl(o),p=Gl(o),y=Vl(s),g=Gl(s),m=Vl(c),v=Gl(c),b={a:function(t){return o[t.getDay()]},A:function(t){return a[t.getDay()]},b:function(t){return c[t.getMonth()]},B:function(t){return s[t.getMonth()]},c:null,d:yh,e:yh,f:_h,g:Dh,G:Bh,H:gh,I:mh,j:vh,L:bh,m:xh,M:wh,p:function(t){return i[+(t.getHours()>=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:nf,s:rf,S:kh,u:Th,U:Eh,V:Sh,w:Ah,W:Mh,x:null,X:null,y:Nh,Y:Oh,Z:Lh,"%":ef},_={a:function(t){return o[t.getUTCDay()]},A:function(t){return a[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:Ih,e:Ih,f:Yh,g:Qh,G:Jh,H:Rh,I:Fh,j:Ph,L:jh,m:zh,M:Uh,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:nf,s:rf,S:qh,u:Hh,U:$h,V:Vh,w:Gh,W:Xh,x:null,X:null,y:Zh,Y:Kh,Z:tf,"%":ef},x={a:function(t,e,n){var r=d.exec(e.slice(n));return r?(t.w=p.get(r[0].toLowerCase()),n+r[0].length):-1},A:function(t,e,n){var r=h.exec(e.slice(n));return r?(t.w=f.get(r[0].toLowerCase()),n+r[0].length):-1},b:function(t,e,n){var r=m.exec(e.slice(n));return r?(t.m=v.get(r[0].toLowerCase()),n+r[0].length):-1},B:function(t,e,n){var r=y.exec(e.slice(n));return r?(t.m=g.get(r[0].toLowerCase()),n+r[0].length):-1},c:function(t,n,r){return T(t,e,n,r)},d:ah,e:ah,f:hh,g:eh,G:th,H:sh,I:sh,j:oh,L:lh,m:ih,M:ch,p:function(t,e,n){var r=u.exec(e.slice(n));return r?(t.p=l.get(r[0].toLowerCase()),n+r[0].length):-1},q:rh,Q:dh,s:ph,S:uh,u:Zl,U:Ql,V:Kl,w:Xl,W:Jl,x:function(t,e,r){return T(t,n,e,r)},X:function(t,e,n){return T(t,r,e,n)},y:eh,Y:th,Z:nh,"%":fh};function w(t,e){return function(n){var r,i,a,o=[],s=-1,c=0,u=t.length;for(n instanceof Date||(n=new Date(+n));++s53)return null;"w"in a||(a.w=1),"Z"in a?(i=(r=Fl(Pl(a.y,0,1))).getUTCDay(),r=i>4||0===i?pl.ceil(r):pl(r),r=xl.offset(r,7*(a.V-1)),a.y=r.getUTCFullYear(),a.m=r.getUTCMonth(),a.d=r.getUTCDate()+(a.w+6)%7):(i=(r=Rl(Pl(a.y,0,1))).getDay(),r=i>4||0===i?Tl.ceil(r):Tl(r),r=Dl.offset(r,7*(a.V-1)),a.y=r.getFullYear(),a.m=r.getMonth(),a.d=r.getDate()+(a.w+6)%7)}else("W"in a||"U"in a)&&("w"in a||(a.w="u"in a?a.u%7:"W"in a?1:0),i="Z"in a?Fl(Pl(a.y,0,1)).getUTCDay():Rl(Pl(a.y,0,1)).getDay(),a.m=0,a.d="W"in a?(a.w+6)%7+7*a.W-(i+5)%7:a.w+7*a.U-(i+6)%7);return"Z"in a?(a.H+=a.Z/100|0,a.M+=a.Z%100,Fl(a)):Rl(a)}}function T(t,e,n,r){for(var i,a,o=0,s=e.length,c=n.length;o=c)return-1;if(37===(i=e.charCodeAt(o++))){if(i=e.charAt(o++),!(a=x[i in zl?e.charAt(o++):i])||(r=a(t,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}return b.x=w(n,b),b.X=w(r,b),b.c=w(e,b),_.x=w(n,_),_.X=w(r,_),_.c=w(e,_),{format:function(t){var e=w(t+="",b);return e.toString=function(){return t},e},parse:function(t){var e=k(t+="",!1);return e.toString=function(){return t},e},utcFormat:function(t){var e=w(t+="",_);return e.toString=function(){return t},e},utcParse:function(t){var e=k(t+="",!0);return e.toString=function(){return t},e}}}({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}),Yl=jl.format,jl.parse,jl.utcFormat,jl.utcParse;var af={value:()=>{}};function of(){for(var t,e=0,n=arguments.length,r={};e=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function uf(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),a=0;a=0&&e._call.call(void 0,t),e=e._next;--pf}()}finally{pf=0,function(){for(var t,e,n=ff,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:ff=e);df=t,Af(r)}(),vf=0}}function Sf(){var t=_f.now(),e=t-mf;e>1e3&&(bf-=e,mf=t)}function Af(t){pf||(yf&&(yf=clearTimeout(yf)),t-vf>24?(t<1/0&&(yf=setTimeout(Cf,t-_f.now()-bf)),gf&&(gf=clearInterval(gf))):(gf||(mf=_f.now(),gf=setInterval(Sf,1e3)),pf=1,xf(Cf)))}function Mf(t,e,n){var r=new Tf;return e=null==e?0:+e,r.restart((n=>{r.stop(),t(n+e)}),e,n),r}Tf.prototype=Ef.prototype={constructor:Tf,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?wf():+n)+(null==e?0:+e),this._next||df===this||(df?df._next=this:ff=this,df=this),this._call=t,this._time=n,Af()},stop:function(){this._call&&(this._call=null,this._time=1/0,Af())}};var Nf=hf("start","end","cancel","interrupt"),Df=[];function Of(t,e,n,r,i,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function a(c){var u,l,h,f;if(1!==n.state)return s();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return Mf(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function Lf(t,e){var n=If(t,e);if(n.state>3)throw new Error("too late; already running");return n}function If(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function Rf(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var Ff,Pf=180/Math.PI,jf={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function Yf(t,e,n,r,i,a){var o,s,c;return(o=Math.sqrt(t*t+e*e))&&(t/=o,e/=o),(c=t*n+e*r)&&(n-=t*c,r-=e*c),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,c/=s),t*r180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(i(n)+"rotate(",null,r)-2,x:Rf(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(a.rotate,o.rotate,s,c),function(t,e,n,a){t!==e?a.push({i:n.push(i(n)+"skewX(",null,r)-2,x:Rf(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(a.skewX,o.skewX,s,c),function(t,e,n,r,a,o){if(t!==n||e!==r){var s=a.push(i(a)+"scale(",null,",",null,")");o.push({i:s-4,x:Rf(t,n)},{i:s-2,x:Rf(e,r)})}else 1===n&&1===r||a.push(i(a)+"scale("+n+","+r+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,c),a=o=null,function(t){for(var e,n=-1,r=c.length;++n=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=ra&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:Rf(n,r)})),a=Qf.lastIndex;return a=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?Bf:Lf;return function(){var o=a(this,t),s=o.on;s!==r&&(i=(r=s).copy()).on(e,n),o.on=i}}var gd=iu.prototype.constructor;function md(t){return function(){this.style.removeProperty(t)}}function vd(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function bd(t,e,n){var r,i;function a(){var a=e.apply(this,arguments);return a!==i&&(r=(i=a)&&vd(t,a,n)),r}return a._value=e,a}function _d(t){return function(e){this.textContent=t.call(this,e)}}function xd(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&_d(r)),e}return r._value=t,r}var wd=0;function kd(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function Td(){return++wd}var Ed=iu.prototype;kd.prototype=function(t){return iu().transition(t)}.prototype={constructor:kd,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=$s(t));for(var r=this._groups,i=r.length,a=new Array(i),o=0;o2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete a[i]):o=!1;o&&delete t.__transition}}(this,t)}))},iu.prototype.transition=function(t){var e,n;t instanceof kd?(e=t._id,t=t._name):(e=Td(),(n=Cd).time=wf(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,a=0;ae?1:t>=e?0:NaN}Yd.prototype={constructor:Yd,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var Vd="http://www.w3.org/1999/xhtml";const Gd={svg:"http://www.w3.org/2000/svg",xhtml:Vd,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function Xd(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),Gd.hasOwnProperty(e)?{space:Gd[e],local:t}:t}function Zd(t){return function(){this.removeAttribute(t)}}function Qd(t){return function(){this.removeAttributeNS(t.space,t.local)}}function Kd(t,e){return function(){this.setAttribute(t,e)}}function Jd(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function tp(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function ep(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function np(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function rp(t){return function(){this.style.removeProperty(t)}}function ip(t,e,n){return function(){this.style.setProperty(t,e,n)}}function ap(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function op(t,e){return t.style.getPropertyValue(e)||np(t).getComputedStyle(t,null).getPropertyValue(e)}function sp(t){return function(){delete this[t]}}function cp(t,e){return function(){this[t]=e}}function up(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function lp(t){return t.trim().split(/^|\s+/)}function hp(t){return t.classList||new fp(t)}function fp(t){this._node=t,this._names=lp(t.getAttribute("class")||"")}function dp(t,e){for(var n=hp(t),r=-1,i=e.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function Lp(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,a=e.length;r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var jp=[null];function Yp(t,e){this._groups=t,this._parents=e}function zp(){return new Yp([[document.documentElement]],jp)}Yp.prototype=zp.prototype={constructor:Yp,select:function(t){"function"!=typeof t&&(t=Md(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=x&&(x=_+1);!(b=g[x])&&++x=0;)(r=i[a])&&(o&&4^r.compareDocumentPosition(o)&&o.parentNode.insertBefore(r,o),o=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=Wd);for(var n=this._groups,r=n.length,i=new Array(r),a=0;a1?this.each((null==e?rp:"function"==typeof e?ap:ip)(t,e,null==n?"":n)):op(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?sp:"function"==typeof e?up:cp)(t,e)):this.node()[t]},classed:function(t,e){var n=lp(t+"");if(arguments.length<2){for(var r=hp(this.node()),i=-1,a=n.length;++i{}};function Hp(){for(var t,e=0,n=arguments.length,r={};e=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function Vp(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),a=0;a=0&&e._call.call(void 0,t),e=e._next;--Kp}()}finally{Kp=0,function(){for(var t,e,n=Zp,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:Zp=e);Qp=t,fy(r)}(),ny=0}}function hy(){var t=iy.now(),e=t-ey;e>1e3&&(ry-=e,ey=t)}function fy(t){Kp||(Jp&&(Jp=clearTimeout(Jp)),t-ny>24?(t<1/0&&(Jp=setTimeout(ly,t-iy.now()-ry)),ty&&(ty=clearInterval(ty))):(ty||(ey=iy.now(),ty=setInterval(hy,1e3)),Kp=1,ay(ly)))}function dy(t,e,n){var r=new cy;return e=null==e?0:+e,r.restart((n=>{r.stop(),t(n+e)}),e,n),r}cy.prototype=uy.prototype={constructor:cy,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?oy():+n)+(null==e?0:+e),this._next||Qp===this||(Qp?Qp._next=this:Zp=this,Qp=this),this._call=t,this._time=n,fy()},stop:function(){this._call&&(this._call=null,this._time=1/0,fy())}};var py=Xp("start","end","cancel","interrupt"),yy=[];function gy(t,e,n,r,i,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function a(c){var u,l,h,f;if(1!==n.state)return s();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return dy(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function vy(t,e){var n=by(t,e);if(n.state>3)throw new Error("too late; already running");return n}function by(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function _y(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var xy,wy=180/Math.PI,ky={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function Ty(t,e,n,r,i,a){var o,s,c;return(o=Math.sqrt(t*t+e*e))&&(t/=o,e/=o),(c=t*n+e*r)&&(n-=t*c,r-=e*c),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,c/=s),t*r180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(i(n)+"rotate(",null,r)-2,x:_y(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(a.rotate,o.rotate,s,c),function(t,e,n,a){t!==e?a.push({i:n.push(i(n)+"skewX(",null,r)-2,x:_y(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(a.skewX,o.skewX,s,c),function(t,e,n,r,a,o){if(t!==n||e!==r){var s=a.push(i(a)+"scale(",null,",",null,")");o.push({i:s-4,x:_y(t,n)},{i:s-2,x:_y(e,r)})}else 1===n&&1===r||a.push(i(a)+"scale("+n+","+r+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,c),a=o=null,function(t){for(var e,n=-1,r=c.length;++n>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?Qy(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?Qy(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Yy.exec(t))?new tg(e[1],e[2],e[3],1):(e=zy.exec(t))?new tg(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Uy.exec(t))?Qy(e[1],e[2],e[3],e[4]):(e=qy.exec(t))?Qy(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Hy.exec(t))?ig(e[1],e[2]/100,e[3]/100,1):(e=$y.exec(t))?ig(e[1],e[2]/100,e[3]/100,e[4]):Wy.hasOwnProperty(t)?Zy(Wy[t]):"transparent"===t?new tg(NaN,NaN,NaN,0):null}function Zy(t){return new tg(t>>16&255,t>>8&255,255&t,1)}function Qy(t,e,n,r){return r<=0&&(t=e=n=NaN),new tg(t,e,n,r)}function Ky(t){return t instanceof By||(t=Xy(t)),t?new tg((t=t.rgb()).r,t.g,t.b,t.opacity):new tg}function Jy(t,e,n,r){return 1===arguments.length?Ky(t):new tg(t,e,n,null==r?1:r)}function tg(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function eg(){return"#"+rg(this.r)+rg(this.g)+rg(this.b)}function ng(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function rg(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function ig(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new og(t,e,n,r)}function ag(t){if(t instanceof og)return new og(t.h,t.s,t.l,t.opacity);if(t instanceof By||(t=Xy(t)),!t)return new og;if(t instanceof og)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new og(o,s,c,t.opacity)}function og(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function sg(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}function cg(t,e,n,r,i){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*r+o*i)/6}Dy(By,Xy,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:Vy,formatHex:Vy,formatHsl:function(){return ag(this).formatHsl()},formatRgb:Gy,toString:Gy}),Dy(tg,Jy,Oy(By,{brighter:function(t){return t=null==t?Iy:Math.pow(Iy,t),new tg(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Ly:Math.pow(Ly,t),new tg(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:eg,formatHex:eg,formatRgb:ng,toString:ng})),Dy(og,(function(t,e,n,r){return 1===arguments.length?ag(t):new og(t,e,n,null==r?1:r)}),Oy(By,{brighter:function(t){return t=null==t?Iy:Math.pow(Iy,t),new og(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Ly:Math.pow(Ly,t),new og(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new tg(sg(t>=240?t-240:t+120,i,r),sg(t,i,r),sg(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const ug=t=>()=>t;function lg(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):ug(isNaN(t)?e:t)}const hg=function t(e){var n=function(t){return 1==(t=+t)?lg:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):ug(isNaN(e)?n:e)}}(e);function r(t,e){var r=n((t=Jy(t)).r,(e=Jy(e)).r),i=n(t.g,e.g),a=n(t.b,e.b),o=lg(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=a(e),t.opacity=o(e),t+""}}return r.gamma=t,r}(1);function fg(t){return function(e){var n,r,i=e.length,a=new Array(i),o=new Array(i),s=new Array(i);for(n=0;n=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=ra&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:_y(n,r)})),a=pg.lastIndex;return a=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?my:vy;return function(){var o=a(this,t),s=o.on;s!==r&&(i=(r=s).copy()).on(e,n),o.on=i}}var Bg=Up.prototype.constructor;function Lg(t){return function(){this.style.removeProperty(t)}}function Ig(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function Rg(t,e,n){var r,i;function a(){var a=e.apply(this,arguments);return a!==i&&(r=(i=a)&&Ig(t,a,n)),r}return a._value=e,a}function Fg(t){return function(e){this.textContent=t.call(this,e)}}function Pg(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&Fg(r)),e}return r._value=t,r}var jg=0;function Yg(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function zg(){return++jg}var Ug=Up.prototype;Yg.prototype=function(t){return Up().transition(t)}.prototype={constructor:Yg,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=Md(t));for(var r=this._groups,i=r.length,a=new Array(i),o=0;o2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete a[i]):o=!1;o&&delete t.__transition}}(this,t)}))},Up.prototype.transition=function(t){var e,n;t instanceof Yg?(e=t._id,t=t._name):(e=zg(),(n=qg).time=oy(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,a=0;a0?tm(fm,--lm):0,cm--,10===hm&&(cm=1,sm--),hm}function ym(){return hm=lm2||bm(hm)>3?"":" "}function wm(t,e){for(;--e&&ym()&&!(hm<48||hm>102||hm>57&&hm<65||hm>70&&hm<97););return vm(t,mm()+(e<6&&32==gm()&&32==ym()))}function km(t){for(;ym();)switch(hm){case t:return lm;case 34:case 39:34!==t&&39!==t&&km(hm);break;case 40:41===t&&km(t);break;case 92:ym()}return lm}function Tm(t,e){for(;ym()&&t+hm!==57&&(t+hm!==84||47!==gm()););return"/*"+vm(e,lm-1)+"*"+Zg(47===t?t:ym())}function Em(t){for(;!bm(gm());)ym();return vm(t,lm)}function Cm(t){return function(t){return fm="",t}(Sm("",null,null,null,[""],t=function(t){return sm=cm=1,um=nm(fm=t),lm=0,[]}(t),0,[0],t))}function Sm(t,e,n,r,i,a,o,s,c){for(var u=0,l=0,h=o,f=0,d=0,p=0,y=1,g=1,m=1,v=0,b="",_=i,x=a,w=r,k=b;g;)switch(p=v,v=ym()){case 40:if(108!=p&&58==k.charCodeAt(h-1)){-1!=Jg(k+=Kg(_m(v),"&","&\f"),"&\f")&&(m=-1);break}case 34:case 39:case 91:k+=_m(v);break;case 9:case 10:case 13:case 32:k+=xm(p);break;case 92:k+=wm(mm()-1,7);continue;case 47:switch(gm()){case 42:case 47:im(Mm(Tm(ym(),mm()),e,n),c);break;default:k+="/"}break;case 123*y:s[u++]=nm(k)*m;case 125*y:case 59:case 0:switch(v){case 0:case 125:g=0;case 59+l:d>0&&nm(k)-h&&im(d>32?Nm(k+";",r,n,h-1):Nm(Kg(k," ","")+";",r,n,h-2),c);break;case 59:k+=";";default:if(im(w=Am(k,e,n,u,l,i,s,b,_=[],x=[],h),a),123===v)if(0===l)Sm(k,e,w,w,_,a,h,s,x);else switch(f){case 100:case 109:case 115:Sm(t,w,w,r&&im(Am(t,w,w,0,0,i,s,b,i,_=[],h),x),i,x,h,s,r?_:x);break;default:Sm(k,w,w,w,[""],x,0,s,x)}}u=l=d=0,y=m=1,b=k="",h=o;break;case 58:h=1+nm(k),d=p;default:if(y<1)if(123==v)--y;else if(125==v&&0==y++&&125==pm())continue;switch(k+=Zg(v),v*y){case 38:m=l>0?1:(k+="\f",-1);break;case 44:s[u++]=(nm(k)-1)*m,m=1;break;case 64:45===gm()&&(k+=_m(ym())),f=gm(),l=h=nm(b=k+=Em(mm())),v++;break;case 45:45===p&&2==nm(k)&&(y=0)}}return a}function Am(t,e,n,r,i,a,o,s,c,u,l){for(var h=i-1,f=0===i?a:[""],d=rm(f),p=0,y=0,g=0;p0?f[m]+" "+v:Kg(v,/&\f/g,f[m])))&&(c[g++]=b);return dm(t,e,n,0===i?Vg:s,c,u,l)}function Mm(t,e,n){return dm(t,e,n,Wg,Zg(hm),em(t,2,-2),0)}function Nm(t,e,n,r){return dm(t,e,n,Gg,em(t,0,r),em(t,r+1,-1),r)}const Dm="8.14.0";var Om=n(9609),Bm=n(7856),Lm=n.n(Bm),Im=function(t){var e=t.replace(/\\u[\dA-F]{4}/gi,(function(t){return String.fromCharCode(parseInt(t.replace(/\\u/g,""),16))}));return e=(e=(e=e.replace(/\\x([0-9a-f]{2})/gi,(function(t,e){return String.fromCharCode(parseInt(e,16))}))).replace(/\\[\d\d\d]{3}/gi,(function(t){return String.fromCharCode(parseInt(t.replace(/\\/g,""),8))}))).replace(/\\[\d\d\d]{2}/gi,(function(t){return String.fromCharCode(parseInt(t.replace(/\\/g,""),8))}))},Rm=function(t){for(var e="",n=0;n>=0;){if(!((n=t.indexOf("=0)){e+=t,n=-1;break}e+=t.substr(0,n),(n=(t=t.substr(n+1)).indexOf("<\/script>"))>=0&&(n+=9,t=t.substr(n))}var r=Im(e);return(r=(r=(r=r.replace(/script>/gi,"#")).replace(/javascript:/gi,"#")).replace(/onerror=/gi,"onerror:")).replace(/')}if(void 0!==n)switch(y){case"flowchart":case"flowchart-v2":n(T,gx.bindFunctions);break;case"gantt":n(T,_w.bindFunctions);break;case"class":case"classDiagram":n(T,bb.bindFunctions);break;default:n(T)}else o.debug("CB = undefined!");nT.forEach((function(t){t()})),nT=[];var S="sandbox"===s.securityLevel?"#i"+t:"#d"+t,A=au(S).node();return null!==A&&"function"==typeof A.remove&&au(S).node().remove(),T},parse:function(t){var e=Jv(),n=Hv.detectInit(t,e);n&&o.debug("reinit ",n);var r,i=Hv.detectType(t,e);switch(o.debug("Type "+i),i){case"git":(r=zw()).parser.yy=Pw;break;case"flowchart":case"flowchart-v2":gx.clear(),(r=vx()).parser.yy=gx;break;case"sequence":(r=Bk()).parser.yy=eT;break;case"gantt":(r=Tw()).parser.yy=_w;break;case"class":case"classDiagram":(r=Eb()).parser.yy=bb;break;case"state":case"stateDiagram":(r=VT()).parser.yy=cE;break;case"info":o.debug("info info info"),(r=nk()).parser.yy=tk;break;case"pie":o.debug("pie"),(r=ok()).parser.yy=lk;break;case"er":o.debug("er"),(r=R_()).parser.yy=L_;break;case"journey":o.debug("Journey"),(r=RE()).parser.yy=LE;break;case"requirement":case"requirementDiagram":o.debug("RequirementDiagram"),(r=yk()).parser.yy=xk}return r.parser.yy.graphType=i,r.parser.yy.parseError=function(t,e){throw{str:t,hash:e}},r.parse(t),r},parseDirective:function(t,e,n,r){try{if(void 0!==e)switch(e=e.trim(),n){case"open_directive":oC={};break;case"type_directive":oC.type=e.toLowerCase();break;case"arg_directive":oC.args=JSON.parse(e);break;case"close_directive":(function(t,e,n){switch(o.debug("Directive type=".concat(e.type," with args:"),e.args),e.type){case"init":case"initialize":["config"].forEach((function(t){void 0!==e.args[t]&&("flowchart-v2"===n&&(n="flowchart"),e.args[n]=e.args[t],delete e.args[t])})),o.debug("sanitize in handleDirective",e.args),Uv(e.args),o.debug("sanitize in handleDirective (done)",e.args),e.args,eb(e.args);break;case"wrap":case"nowrap":t&&t.setWrap&&t.setWrap("wrap"===e.type);break;case"themeCss":o.warn("themeCss encountered");break;default:o.warn("Unhandled directive: source: '%%{".concat(e.type,": ").concat(JSON.stringify(e.args?e.args:{}),"}%%"),e)}})(t,oC,r),oC=null}}catch(t){o.error("Error while rendering sequenceDiagram directive: ".concat(e," jison context: ").concat(n)),o.error(t.message)}},initialize:function(t){t&&t.fontFamily&&(t.themeVariables&&t.themeVariables.fontFamily||(t.themeVariables={fontFamily:t.fontFamily})),function(t){Wv=Lv({},t)}(t),t&&t.theme&&ov[t.theme]?t.themeVariables=ov[t.theme].getThemeVariables(t.themeVariables):t&&(t.themeVariables=ov.default.getThemeVariables(t.themeVariables));var e="object"===iC(t)?function(t){return Gv=Lv({},Vv),Gv=Lv(Gv,t),t.theme&&(Gv.themeVariables=ov[t.theme].getThemeVariables(t.themeVariables)),Zv=Qv(Gv,Xv),Gv}(t):Kv();sC(e),s(e.logLevel)},reinitialize:function(){},getConfig:Jv,setConfig:function(t){return Lv(Zv,t),Jv()},getSiteConfig:Kv,updateSiteConfig:function(t){return Gv=Lv(Gv,t),Qv(Gv,Xv),Gv},reset:function(){nb()},globalReset:function(){nb(),sC(Jv())},defaultConfig:Vv});s(Jv().logLevel),nb(Jv());const uC=cC;var lC=function(){hC.startOnLoad?uC.getConfig().startOnLoad&&hC.init():void 0===hC.startOnLoad&&(o.debug("In start, no config"),uC.getConfig().startOnLoad&&hC.init())};"undefined"!=typeof document&&window.addEventListener("load",(function(){lC()}),!1);var hC={startOnLoad:!0,htmlLabels:!0,mermaidAPI:uC,parse:uC.parse,render:uC.render,init:function(){var t,e,n=this,r=uC.getConfig();arguments.length>=2?(void 0!==arguments[0]&&(hC.sequenceConfig=arguments[0]),t=arguments[1]):t=arguments[0],"function"==typeof arguments[arguments.length-1]?(e=arguments[arguments.length-1],o.debug("Callback function found")):void 0!==r.mermaid&&("function"==typeof r.mermaid.callback?(e=r.mermaid.callback,o.debug("Callback function found")):o.debug("No Callback function found")),t=void 0===t?document.querySelectorAll(".mermaid"):"string"==typeof t?document.querySelectorAll(t):t instanceof window.Node?[t]:t,o.debug("Start On Load before: "+hC.startOnLoad),void 0!==hC.startOnLoad&&(o.debug("Start On Load inner: "+hC.startOnLoad),uC.updateSiteConfig({startOnLoad:hC.startOnLoad})),void 0!==hC.ganttConfig&&uC.updateSiteConfig({gantt:hC.ganttConfig});for(var i,a=new Hv.initIdGeneratior(r.deterministicIds,r.deterministicIDSeed),s=function(r){var s=t[r];if(s.getAttribute("data-processed"))return"continue";s.setAttribute("data-processed",!0);var c="mermaid-".concat(a.next());i=s.innerHTML,i=Hv.entityDecode(i).trim().replace(//gi,"
");var u=Hv.detectInit(i);u&&o.debug("Detected early reinit: ",u);try{uC.render(c,i,(function(t,n){s.innerHTML=t,void 0!==e&&e(c),n&&n(s)}),s)}catch(t){o.warn("Syntax Error rendering"),o.warn(t),n.parseError&&n.parseError(t)}},c=0;c{t.exports={graphlib:n(6614),dagre:n(1463),intersect:n(8114),render:n(5787),util:n(8355),version:n(5689)}},9144:(t,e,n)=>{var r=n(8355);function i(t,e,n,i){var a=t.append("marker").attr("id",e).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerUnits","strokeWidth").attr("markerWidth",8).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").style("stroke-width",1).style("stroke-dasharray","1,0");r.applyStyle(a,n[i+"Style"]),n[i+"Class"]&&a.attr("class",n[i+"Class"])}t.exports={default:i,normal:i,vee:function(t,e,n,i){var a=t.append("marker").attr("id",e).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerUnits","strokeWidth").attr("markerWidth",8).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 L 4 5 z").style("stroke-width",1).style("stroke-dasharray","1,0");r.applyStyle(a,n[i+"Style"]),n[i+"Class"]&&a.attr("class",n[i+"Class"])},undirected:function(t,e,n,i){var a=t.append("marker").attr("id",e).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerUnits","strokeWidth").attr("markerWidth",8).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M 0 5 L 10 5").style("stroke-width",1).style("stroke-dasharray","1,0");r.applyStyle(a,n[i+"Style"]),n[i+"Class"]&&a.attr("class",n[i+"Class"])}}},5632:(t,e,n)=>{var r=n(8355),i=n(4322),a=n(1322);t.exports=function(t,e){var n,o=e.nodes().filter((function(t){return r.isSubgraph(e,t)})),s=t.selectAll("g.cluster").data(o,(function(t){return t}));return s.selectAll("*").remove(),s.enter().append("g").attr("class","cluster").attr("id",(function(t){return e.node(t).id})).style("opacity",0),s=t.selectAll("g.cluster"),r.applyTransition(s,e).style("opacity",1),s.each((function(t){var n=e.node(t),r=i.select(this);i.select(this).append("rect");var o=r.append("g").attr("class","label");a(o,n,n.clusterLabelPos)})),s.selectAll("rect").each((function(t){var n=e.node(t),a=i.select(this);r.applyStyle(a,n.style)})),n=s.exit?s.exit():s.selectAll(null),r.applyTransition(n,e).style("opacity",0).remove(),s}},6315:(t,e,n)=>{"use strict";var r=n(1034),i=n(1322),a=n(8355),o=n(4322);t.exports=function(t,e){var n,s=t.selectAll("g.edgeLabel").data(e.edges(),(function(t){return a.edgeToId(t)})).classed("update",!0);return s.exit().remove(),s.enter().append("g").classed("edgeLabel",!0).style("opacity",0),(s=t.selectAll("g.edgeLabel")).each((function(t){var n=o.select(this);n.select(".label").remove();var a=e.edge(t),s=i(n,e.edge(t),0,0).classed("label",!0),c=s.node().getBBox();a.labelId&&s.attr("id",a.labelId),r.has(a,"width")||(a.width=c.width),r.has(a,"height")||(a.height=c.height)})),n=s.exit?s.exit():s.selectAll(null),a.applyTransition(n,e).style("opacity",0).remove(),s}},940:(t,e,n)=>{"use strict";var r=n(1034),i=n(7584),a=n(8355),o=n(4322);function s(t,e){var n=(o.line||o.svg.line)().x((function(t){return t.x})).y((function(t){return t.y}));return(n.curve||n.interpolate)(t.curve),n(e)}t.exports=function(t,e,n){var c=t.selectAll("g.edgePath").data(e.edges(),(function(t){return a.edgeToId(t)})).classed("update",!0),u=function(t,e){var n=t.enter().append("g").attr("class","edgePath").style("opacity",0);return n.append("path").attr("class","path").attr("d",(function(t){var n=e.edge(t),i=e.node(t.v).elem;return s(n,r.range(n.points.length).map((function(){return e=(t=i).getBBox(),{x:(n=t.ownerSVGElement.getScreenCTM().inverse().multiply(t.getScreenCTM()).translate(e.width/2,e.height/2)).e,y:n.f};var t,e,n})))})),n.append("defs"),n}(c,e);!function(t,e){var n=t.exit();a.applyTransition(n,e).style("opacity",0).remove()}(c,e);var l=void 0!==c.merge?c.merge(u):c;return a.applyTransition(l,e).style("opacity",1),l.each((function(t){var n=o.select(this),r=e.edge(t);r.elem=this,r.id&&n.attr("id",r.id),a.applyClass(n,r.class,(n.classed("update")?"update ":"")+"edgePath")})),l.selectAll("path.path").each((function(t){var n=e.edge(t);n.arrowheadId=r.uniqueId("arrowhead");var c=o.select(this).attr("marker-end",(function(){return"url("https://github.com/+(t=location.href,e=n.arrowheadId,t.split(%22#")[0]+"#"+e+")");var t,e})).style("fill","none");a.applyTransition(c,e).attr("d",(function(t){return function(t,e){var n=t.edge(e),r=t.node(e.v),a=t.node(e.w),o=n.points.slice(1,n.points.length-1);return o.unshift(i(r,o[0])),o.push(i(a,o[o.length-1])),s(n,o)}(e,t)})),a.applyStyle(c,n.style)})),l.selectAll("defs *").remove(),l.selectAll("defs").each((function(t){var r=e.edge(t);(0,n[r.arrowhead])(o.select(this),r.arrowheadId,r,"arrowhead")})),l}},607:(t,e,n)=>{"use strict";var r=n(1034),i=n(1322),a=n(8355),o=n(4322);t.exports=function(t,e,n){var s,c=e.nodes().filter((function(t){return!a.isSubgraph(e,t)})),u=t.selectAll("g.node").data(c,(function(t){return t})).classed("update",!0);return u.exit().remove(),u.enter().append("g").attr("class","node").style("opacity",0),(u=t.selectAll("g.node")).each((function(t){var s=e.node(t),c=o.select(this);a.applyClass(c,s.class,(c.classed("update")?"update ":"")+"node"),c.select("g.label").remove();var u=c.append("g").attr("class","label"),l=i(u,s),h=n[s.shape],f=r.pick(l.node().getBBox(),"width","height");s.elem=this,s.id&&c.attr("id",s.id),s.labelId&&u.attr("id",s.labelId),r.has(s,"width")&&(f.width=s.width),r.has(s,"height")&&(f.height=s.height),f.width+=s.paddingLeft+s.paddingRight,f.height+=s.paddingTop+s.paddingBottom,u.attr("transform","translate("+(s.paddingLeft-s.paddingRight)/2+","+(s.paddingTop-s.paddingBottom)/2+")");var d=o.select(this);d.select(".label-container").remove();var p=h(d,f,s).classed("label-container",!0);a.applyStyle(p,s.style);var y=p.node().getBBox();s.width=y.width,s.height=y.height})),s=u.exit?u.exit():u.selectAll(null),a.applyTransition(s,e).style("opacity",0).remove(),u}},4322:(t,e,n)=>{var r;if(!r)try{r=n(7188)}catch(t){}r||(r=window.d3),t.exports=r},1463:(t,e,n)=>{var r;try{r=n(681)}catch(t){}r||(r=window.dagre),t.exports=r},6614:(t,e,n)=>{var r;try{r=n(8282)}catch(t){}r||(r=window.graphlib),t.exports=r},8114:(t,e,n)=>{t.exports={node:n(7584),circle:n(6587),ellipse:n(3260),polygon:n(5337),rect:n(8049)}},6587:(t,e,n)=>{var r=n(3260);t.exports=function(t,e,n){return r(t,e,e,n)}},3260:t=>{t.exports=function(t,e,n,r){var i=t.x,a=t.y,o=i-r.x,s=a-r.y,c=Math.sqrt(e*e*s*s+n*n*o*o),u=Math.abs(e*n*o/c);r.x{function e(t,e){return t*e>0}t.exports=function(t,n,r,i){var a,o,s,c,u,l,h,f,d,p,y,g,m;if(!(a=n.y-t.y,s=t.x-n.x,u=n.x*t.y-t.x*n.y,d=a*r.x+s*r.y+u,p=a*i.x+s*i.y+u,0!==d&&0!==p&&e(d,p)||(o=i.y-r.y,c=r.x-i.x,l=i.x*r.y-r.x*i.y,h=o*t.x+c*t.y+l,f=o*n.x+c*n.y+l,0!==h&&0!==f&&e(h,f)||0==(y=a*c-o*s))))return g=Math.abs(y/2),{x:(m=s*l-c*u)<0?(m-g)/y:(m+g)/y,y:(m=o*u-a*l)<0?(m-g)/y:(m+g)/y}}},7584:t=>{t.exports=function(t,e){return t.intersect(e)}},5337:(t,e,n)=>{var r=n(6808);t.exports=function(t,e,n){var i=t.x,a=t.y,o=[],s=Number.POSITIVE_INFINITY,c=Number.POSITIVE_INFINITY;e.forEach((function(t){s=Math.min(s,t.x),c=Math.min(c,t.y)}));for(var u=i-t.width/2-s,l=a-t.height/2-c,h=0;h1&&o.sort((function(t,e){var r=t.x-n.x,i=t.y-n.y,a=Math.sqrt(r*r+i*i),o=e.x-n.x,s=e.y-n.y,c=Math.sqrt(o*o+s*s);return a{t.exports=function(t,e){var n,r,i=t.x,a=t.y,o=e.x-i,s=e.y-a,c=t.width/2,u=t.height/2;return Math.abs(s)*c>Math.abs(o)*u?(s<0&&(u=-u),n=0===s?0:u*o/s,r=u):(o<0&&(c=-c),n=c,r=0===o?0:c*s/o),{x:i+n,y:a+r}}},8284:(t,e,n)=>{var r=n(8355);t.exports=function(t,e){var n=t.append("foreignObject").attr("width","100000"),i=n.append("xhtml:div");i.attr("xmlns","http://www.w3.org/1999/xhtml");var a=e.label;switch(typeof a){case"function":i.insert(a);break;case"object":i.insert((function(){return a}));break;default:i.html(a)}r.applyStyle(i,e.labelStyle),i.style("display","inline-block"),i.style("white-space","nowrap");var o=i.node().getBoundingClientRect();return n.attr("width",o.width).attr("height",o.height),n}},1322:(t,e,n)=>{var r=n(7318),i=n(8284),a=n(8287);t.exports=function(t,e,n){var o=e.label,s=t.append("g");"svg"===e.labelType?a(s,e):"string"!=typeof o||"html"===e.labelType?i(s,e):r(s,e);var c,u=s.node().getBBox();switch(n){case"top":c=-e.height/2;break;case"bottom":c=e.height/2-u.height;break;default:c=-u.height/2}return s.attr("transform","translate("+-u.width/2+","+c+")"),s}},8287:(t,e,n)=>{var r=n(8355);t.exports=function(t,e){var n=t;return n.node().appendChild(e.label),r.applyStyle(n,e.labelStyle),n}},7318:(t,e,n)=>{var r=n(8355);t.exports=function(t,e){for(var n=t.append("text"),i=function(t){for(var e,n="",r=!1,i=0;i{var r;try{r={defaults:n(1747),each:n(6073),isFunction:n(3560),isPlainObject:n(8630),pick:n(9722),has:n(8721),range:n(6026),uniqueId:n(3955)}}catch(t){}r||(r=window._),t.exports=r},6381:(t,e,n)=>{"use strict";var r=n(8355),i=n(4322);t.exports=function(t,e){var n=t.filter((function(){return!i.select(this).classed("update")}));function a(t){var n=e.node(t);return"translate("+n.x+","+n.y+")"}n.attr("transform",a),r.applyTransition(t,e).style("opacity",1).attr("transform",a),r.applyTransition(n.selectAll("rect"),e).attr("width",(function(t){return e.node(t).width})).attr("height",(function(t){return e.node(t).height})).attr("x",(function(t){return-e.node(t).width/2})).attr("y",(function(t){return-e.node(t).height/2}))}},4577:(t,e,n)=>{"use strict";var r=n(8355),i=n(4322),a=n(1034);t.exports=function(t,e){function n(t){var n=e.edge(t);return a.has(n,"x")?"translate("+n.x+","+n.y+")":""}t.filter((function(){return!i.select(this).classed("update")})).attr("transform",n),r.applyTransition(t,e).style("opacity",1).attr("transform",n)}},4849:(t,e,n)=>{"use strict";var r=n(8355),i=n(4322);t.exports=function(t,e){function n(t){var n=e.node(t);return"translate("+n.x+","+n.y+")"}t.filter((function(){return!i.select(this).classed("update")})).attr("transform",n),r.applyTransition(t,e).style("opacity",1).attr("transform",n)}},5787:(t,e,n)=>{var r=n(1034),i=n(4322),a=n(1463).layout;t.exports=function(){var t=n(607),e=n(5632),i=n(6315),u=n(940),l=n(4849),h=n(4577),f=n(6381),d=n(4418),p=n(9144),y=function(n,y){!function(t){t.nodes().forEach((function(e){var n=t.node(e);r.has(n,"label")||t.children(e).length||(n.label=e),r.has(n,"paddingX")&&r.defaults(n,{paddingLeft:n.paddingX,paddingRight:n.paddingX}),r.has(n,"paddingY")&&r.defaults(n,{paddingTop:n.paddingY,paddingBottom:n.paddingY}),r.has(n,"padding")&&r.defaults(n,{paddingLeft:n.padding,paddingRight:n.padding,paddingTop:n.padding,paddingBottom:n.padding}),r.defaults(n,o),r.each(["paddingLeft","paddingRight","paddingTop","paddingBottom"],(function(t){n[t]=Number(n[t])})),r.has(n,"width")&&(n._prevWidth=n.width),r.has(n,"height")&&(n._prevHeight=n.height)})),t.edges().forEach((function(e){var n=t.edge(e);r.has(n,"label")||(n.label=""),r.defaults(n,s)}))}(y);var g=c(n,"output"),m=c(g,"clusters"),v=c(g,"edgePaths"),b=i(c(g,"edgeLabels"),y),_=t(c(g,"nodes"),y,d);a(y),l(_,y),h(b,y),u(v,y,p);var x=e(m,y);f(x,y),function(t){r.each(t.nodes(),(function(e){var n=t.node(e);r.has(n,"_prevWidth")?n.width=n._prevWidth:delete n.width,r.has(n,"_prevHeight")?n.height=n._prevHeight:delete n.height,delete n._prevWidth,delete n._prevHeight}))}(y)};return y.createNodes=function(e){return arguments.length?(t=e,y):t},y.createClusters=function(t){return arguments.length?(e=t,y):e},y.createEdgeLabels=function(t){return arguments.length?(i=t,y):i},y.createEdgePaths=function(t){return arguments.length?(u=t,y):u},y.shapes=function(t){return arguments.length?(d=t,y):d},y.arrows=function(t){return arguments.length?(p=t,y):p},y};var o={paddingLeft:10,paddingRight:10,paddingTop:10,paddingBottom:10,rx:0,ry:0,shape:"rect"},s={arrowhead:"normal",curve:i.curveLinear};function c(t,e){var n=t.select("g."+e);return n.empty()&&(n=t.append("g").attr("class",e)),n}},4418:(t,e,n)=>{"use strict";var r=n(8049),i=n(3260),a=n(6587),o=n(5337);t.exports={rect:function(t,e,n){var i=t.insert("rect",":first-child").attr("rx",n.rx).attr("ry",n.ry).attr("x",-e.width/2).attr("y",-e.height/2).attr("width",e.width).attr("height",e.height);return n.intersect=function(t){return r(n,t)},i},ellipse:function(t,e,n){var r=e.width/2,a=e.height/2,o=t.insert("ellipse",":first-child").attr("x",-e.width/2).attr("y",-e.height/2).attr("rx",r).attr("ry",a);return n.intersect=function(t){return i(n,r,a,t)},o},circle:function(t,e,n){var r=Math.max(e.width,e.height)/2,i=t.insert("circle",":first-child").attr("x",-e.width/2).attr("y",-e.height/2).attr("r",r);return n.intersect=function(t){return a(n,r,t)},i},diamond:function(t,e,n){var r=e.width*Math.SQRT2/2,i=e.height*Math.SQRT2/2,a=[{x:0,y:-i},{x:-r,y:0},{x:0,y:i},{x:r,y:0}],s=t.insert("polygon",":first-child").attr("points",a.map((function(t){return t.x+","+t.y})).join(" "));return n.intersect=function(t){return o(n,a,t)},s}}},8355:(t,e,n)=>{var r=n(1034);t.exports={isSubgraph:function(t,e){return!!t.children(e).length},edgeToId:function(t){return a(t.v)+":"+a(t.w)+":"+a(t.name)},applyStyle:function(t,e){e&&t.attr("style",e)},applyClass:function(t,e,n){e&&t.attr("class",e).attr("class",n+" "+t.attr("class"))},applyTransition:function(t,e){var n=e.graph();if(r.isPlainObject(n)){var i=n.transition;if(r.isFunction(i))return i(t)}return t}};var i=/:/g;function a(t){return t?String(t).replace(i,"\\:"):""}},5689:t=>{t.exports="0.6.4"},7188:(t,e,n)=>{"use strict";n.r(e),n.d(e,{FormatSpecifier:()=>uc,active:()=>Jr,arc:()=>fx,area:()=>vx,areaRadial:()=>Sx,ascending:()=>i,autoType:()=>Fo,axisBottom:()=>it,axisLeft:()=>at,axisRight:()=>rt,axisTop:()=>nt,bisect:()=>u,bisectLeft:()=>c,bisectRight:()=>s,bisector:()=>a,blob:()=>ms,brush:()=>Ai,brushSelection:()=>Ei,brushX:()=>Ci,brushY:()=>Si,buffer:()=>bs,chord:()=>Fi,clientPoint:()=>Dn,cluster:()=>Sd,color:()=>Ve,contourDensity:()=>oo,contours:()=>to,create:()=>j_,creator:()=>ie,cross:()=>f,csv:()=>Ts,csvFormat:()=>To,csvFormatBody:()=>Eo,csvFormatRow:()=>So,csvFormatRows:()=>Co,csvFormatValue:()=>Ao,csvParse:()=>wo,csvParseRows:()=>ko,cubehelix:()=>Ha,curveBasis:()=>sw,curveBasisClosed:()=>uw,curveBasisOpen:()=>hw,curveBundle:()=>dw,curveCardinal:()=>gw,curveCardinalClosed:()=>vw,curveCardinalOpen:()=>_w,curveCatmullRom:()=>kw,curveCatmullRomClosed:()=>Ew,curveCatmullRomOpen:()=>Sw,curveLinear:()=>px,curveLinearClosed:()=>Mw,curveMonotoneX:()=>Fw,curveMonotoneY:()=>Pw,curveNatural:()=>zw,curveStep:()=>qw,curveStepAfter:()=>$w,curveStepBefore:()=>Hw,customEvent:()=>ge,descending:()=>d,deviation:()=>g,dispatch:()=>ft,drag:()=>po,dragDisable:()=>Se,dragEnable:()=>Ae,dsv:()=>ks,dsvFormat:()=>_o,easeBack:()=>hs,easeBackIn:()=>us,easeBackInOut:()=>hs,easeBackOut:()=>ls,easeBounce:()=>os,easeBounceIn:()=>as,easeBounceInOut:()=>ss,easeBounceOut:()=>os,easeCircle:()=>rs,easeCircleIn:()=>es,easeCircleInOut:()=>rs,easeCircleOut:()=>ns,easeCubic:()=>Xr,easeCubicIn:()=>Vr,easeCubicInOut:()=>Xr,easeCubicOut:()=>Gr,easeElastic:()=>ps,easeElasticIn:()=>ds,easeElasticInOut:()=>ys,easeElasticOut:()=>ps,easeExp:()=>ts,easeExpIn:()=>Ko,easeExpInOut:()=>ts,easeExpOut:()=>Jo,easeLinear:()=>jo,easePoly:()=>$o,easePolyIn:()=>qo,easePolyInOut:()=>$o,easePolyOut:()=>Ho,easeQuad:()=>Uo,easeQuadIn:()=>Yo,easeQuadInOut:()=>Uo,easeQuadOut:()=>zo,easeSin:()=>Zo,easeSinIn:()=>Go,easeSinInOut:()=>Zo,easeSinOut:()=>Xo,entries:()=>pa,event:()=>le,extent:()=>m,forceCenter:()=>Bs,forceCollide:()=>Ws,forceLink:()=>Xs,forceManyBody:()=>tc,forceRadial:()=>ec,forceSimulation:()=>Js,forceX:()=>nc,forceY:()=>rc,format:()=>pc,formatDefaultLocale:()=>bc,formatLocale:()=>vc,formatPrefix:()=>yc,formatSpecifier:()=>cc,geoAlbers:()=>Uf,geoAlbersUsa:()=>qf,geoArea:()=>yu,geoAzimuthalEqualArea:()=>Vf,geoAzimuthalEqualAreaRaw:()=>Wf,geoAzimuthalEquidistant:()=>Xf,geoAzimuthalEquidistantRaw:()=>Gf,geoBounds:()=>sl,geoCentroid:()=>bl,geoCircle:()=>Nl,geoClipAntimeridian:()=>Ul,geoClipCircle:()=>ql,geoClipExtent:()=>Vl,geoClipRectangle:()=>Wl,geoConicConformal:()=>ed,geoConicConformalRaw:()=>td,geoConicEqualArea:()=>zf,geoConicEqualAreaRaw:()=>Yf,geoConicEquidistant:()=>ad,geoConicEquidistantRaw:()=>id,geoContains:()=>ph,geoDistance:()=>ah,geoEqualEarth:()=>fd,geoEqualEarthRaw:()=>hd,geoEquirectangular:()=>rd,geoEquirectangularRaw:()=>nd,geoGnomonic:()=>pd,geoGnomonicRaw:()=>dd,geoGraticule:()=>mh,geoGraticule10:()=>vh,geoIdentity:()=>yd,geoInterpolate:()=>bh,geoLength:()=>nh,geoMercator:()=>Qf,geoMercatorRaw:()=>Zf,geoNaturalEarth1:()=>md,geoNaturalEarth1Raw:()=>gd,geoOrthographic:()=>bd,geoOrthographicRaw:()=>vd,geoPath:()=>kf,geoProjection:()=>Ff,geoProjectionMutator:()=>Pf,geoRotation:()=>Sl,geoStereographic:()=>xd,geoStereographicRaw:()=>_d,geoStream:()=>nu,geoTransform:()=>Tf,geoTransverseMercator:()=>kd,geoTransverseMercatorRaw:()=>wd,gray:()=>ka,hcl:()=>Oa,hierarchy:()=>Md,histogram:()=>D,hsl:()=>an,html:()=>Ds,image:()=>Cs,interpolate:()=>Mn,interpolateArray:()=>xn,interpolateBasis:()=>un,interpolateBasisClosed:()=>ln,interpolateBlues:()=>f_,interpolateBrBG:()=>Tb,interpolateBuGn:()=>Ub,interpolateBuPu:()=>Hb,interpolateCividis:()=>k_,interpolateCool:()=>C_,interpolateCubehelix:()=>zp,interpolateCubehelixDefault:()=>T_,interpolateCubehelixLong:()=>Up,interpolateDate:()=>kn,interpolateDiscrete:()=>Sp,interpolateGnBu:()=>Wb,interpolateGreens:()=>p_,interpolateGreys:()=>g_,interpolateHcl:()=>Pp,interpolateHclLong:()=>jp,interpolateHsl:()=>Lp,interpolateHslLong:()=>Ip,interpolateHue:()=>Ap,interpolateInferno:()=>F_,interpolateLab:()=>Rp,interpolateMagma:()=>R_,interpolateNumber:()=>Tn,interpolateNumberArray:()=>bn,interpolateObject:()=>En,interpolateOrRd:()=>Gb,interpolateOranges:()=>w_,interpolatePRGn:()=>Cb,interpolatePiYG:()=>Ab,interpolatePlasma:()=>P_,interpolatePuBu:()=>Kb,interpolatePuBuGn:()=>Zb,interpolatePuOr:()=>Nb,interpolatePuRd:()=>t_,interpolatePurples:()=>v_,interpolateRainbow:()=>A_,interpolateRdBu:()=>Ob,interpolateRdGy:()=>Lb,interpolateRdPu:()=>n_,interpolateRdYlBu:()=>Rb,interpolateRdYlGn:()=>Pb,interpolateReds:()=>__,interpolateRgb:()=>yn,interpolateRgbBasis:()=>mn,interpolateRgbBasisClosed:()=>vn,interpolateRound:()=>Mp,interpolateSinebow:()=>O_,interpolateSpectral:()=>Yb,interpolateString:()=>An,interpolateTransformCss:()=>pr,interpolateTransformSvg:()=>yr,interpolateTurbo:()=>B_,interpolateViridis:()=>I_,interpolateWarm:()=>E_,interpolateYlGn:()=>o_,interpolateYlGnBu:()=>i_,interpolateYlOrBr:()=>c_,interpolateYlOrRd:()=>l_,interpolateZoom:()=>Op,interrupt:()=>ar,interval:()=>fk,isoFormat:()=>uk,isoParse:()=>hk,json:()=>As,keys:()=>fa,lab:()=>Ta,lch:()=>Da,line:()=>mx,lineRadial:()=>Cx,linkHorizontal:()=>Rx,linkRadial:()=>Px,linkVertical:()=>Fx,local:()=>z_,map:()=>na,matcher:()=>mt,max:()=>I,mean:()=>R,median:()=>F,merge:()=>P,min:()=>j,mouse:()=>Bn,namespace:()=>Et,namespaces:()=>Tt,nest:()=>ra,now:()=>Hn,pack:()=>tp,packEnclose:()=>Id,packSiblings:()=>Gd,pairs:()=>l,partition:()=>op,path:()=>Wi,permute:()=>Y,pie:()=>xx,piecewise:()=>qp,pointRadial:()=>Ax,polygonArea:()=>$p,polygonCentroid:()=>Wp,polygonContains:()=>Qp,polygonHull:()=>Zp,polygonLength:()=>Kp,precisionFixed:()=>_c,precisionPrefix:()=>xc,precisionRound:()=>wc,quadtree:()=>Ys,quantile:()=>O,quantize:()=>Hp,radialArea:()=>Sx,radialLine:()=>Cx,randomBates:()=>iy,randomExponential:()=>ay,randomIrwinHall:()=>ry,randomLogNormal:()=>ny,randomNormal:()=>ey,randomUniform:()=>ty,range:()=>k,rgb:()=>Qe,ribbon:()=>Ki,scaleBand:()=>dy,scaleDiverging:()=>ob,scaleDivergingLog:()=>sb,scaleDivergingPow:()=>ub,scaleDivergingSqrt:()=>lb,scaleDivergingSymlog:()=>cb,scaleIdentity:()=>My,scaleImplicit:()=>hy,scaleLinear:()=>Ay,scaleLog:()=>Py,scaleOrdinal:()=>fy,scalePoint:()=>yy,scalePow:()=>Vy,scaleQuantile:()=>Xy,scaleQuantize:()=>Zy,scaleSequential:()=>Jv,scaleSequentialLog:()=>tb,scaleSequentialPow:()=>nb,scaleSequentialQuantile:()=>ib,scaleSequentialSqrt:()=>rb,scaleSequentialSymlog:()=>eb,scaleSqrt:()=>Gy,scaleSymlog:()=>Uy,scaleThreshold:()=>Qy,scaleTime:()=>Yv,scaleUtc:()=>Zv,scan:()=>z,schemeAccent:()=>db,schemeBlues:()=>h_,schemeBrBG:()=>kb,schemeBuGn:()=>zb,schemeBuPu:()=>qb,schemeCategory10:()=>fb,schemeDark2:()=>pb,schemeGnBu:()=>$b,schemeGreens:()=>d_,schemeGreys:()=>y_,schemeOrRd:()=>Vb,schemeOranges:()=>x_,schemePRGn:()=>Eb,schemePaired:()=>yb,schemePastel1:()=>gb,schemePastel2:()=>mb,schemePiYG:()=>Sb,schemePuBu:()=>Qb,schemePuBuGn:()=>Xb,schemePuOr:()=>Mb,schemePuRd:()=>Jb,schemePurples:()=>m_,schemeRdBu:()=>Db,schemeRdGy:()=>Bb,schemeRdPu:()=>e_,schemeRdYlBu:()=>Ib,schemeRdYlGn:()=>Fb,schemeReds:()=>b_,schemeSet1:()=>vb,schemeSet2:()=>bb,schemeSet3:()=>_b,schemeSpectral:()=>jb,schemeTableau10:()=>xb,schemeYlGn:()=>a_,schemeYlGnBu:()=>r_,schemeYlOrBr:()=>s_,schemeYlOrRd:()=>u_,select:()=>Te,selectAll:()=>q_,selection:()=>ke,selector:()=>pt,selectorAll:()=>gt,set:()=>ha,shuffle:()=>U,stack:()=>Xw,stackOffsetDiverging:()=>Qw,stackOffsetExpand:()=>Zw,stackOffsetNone:()=>Ww,stackOffsetSilhouette:()=>Kw,stackOffsetWiggle:()=>Jw,stackOrderAppearance:()=>tk,stackOrderAscending:()=>nk,stackOrderDescending:()=>ik,stackOrderInsideOut:()=>ak,stackOrderNone:()=>Vw,stackOrderReverse:()=>ok,stratify:()=>hp,style:()=>Rt,sum:()=>q,svg:()=>Os,symbol:()=>rw,symbolCircle:()=>jx,symbolCross:()=>Yx,symbolDiamond:()=>qx,symbolSquare:()=>Gx,symbolStar:()=>Vx,symbolTriangle:()=>Zx,symbolWye:()=>ew,symbols:()=>nw,text:()=>xs,thresholdFreedmanDiaconis:()=>B,thresholdScott:()=>L,thresholdSturges:()=>N,tickFormat:()=>Cy,tickIncrement:()=>A,tickStep:()=>M,ticks:()=>S,timeDay:()=>Ag,timeDays:()=>Mg,timeFormat:()=>pm,timeFormatDefaultLocale:()=>Iv,timeFormatLocale:()=>fm,timeFriday:()=>vg,timeFridays:()=>Eg,timeHour:()=>Dg,timeHours:()=>Og,timeInterval:()=>tg,timeMillisecond:()=>Yg,timeMilliseconds:()=>zg,timeMinute:()=>Lg,timeMinutes:()=>Ig,timeMonday:()=>pg,timeMondays:()=>xg,timeMonth:()=>ag,timeMonths:()=>og,timeParse:()=>ym,timeSaturday:()=>bg,timeSaturdays:()=>Cg,timeSecond:()=>Fg,timeSeconds:()=>Pg,timeSunday:()=>dg,timeSundays:()=>_g,timeThursday:()=>mg,timeThursdays:()=>Tg,timeTuesday:()=>yg,timeTuesdays:()=>wg,timeWednesday:()=>gg,timeWednesdays:()=>kg,timeWeek:()=>dg,timeWeeks:()=>_g,timeYear:()=>ng,timeYears:()=>rg,timeout:()=>Kn,timer:()=>Vn,timerFlush:()=>Gn,touch:()=>On,touches:()=>H_,transition:()=>Hr,transpose:()=>H,tree:()=>vp,treemap:()=>kp,treemapBinary:()=>Tp,treemapDice:()=>ap,treemapResquarify:()=>Cp,treemapSlice:()=>bp,treemapSliceDice:()=>Ep,treemapSquarify:()=>wp,tsv:()=>Es,tsvFormat:()=>Oo,tsvFormatBody:()=>Bo,tsvFormatRow:()=>Io,tsvFormatRows:()=>Lo,tsvFormatValue:()=>Ro,tsvParse:()=>No,tsvParseRows:()=>Do,utcDay:()=>im,utcDays:()=>am,utcFormat:()=>gm,utcFriday:()=>Gg,utcFridays:()=>em,utcHour:()=>$v,utcHours:()=>Wv,utcMillisecond:()=>Yg,utcMilliseconds:()=>zg,utcMinute:()=>Gv,utcMinutes:()=>Xv,utcMonday:()=>Hg,utcMondays:()=>Qg,utcMonth:()=>Uv,utcMonths:()=>qv,utcParse:()=>mm,utcSaturday:()=>Xg,utcSaturdays:()=>nm,utcSecond:()=>Fg,utcSeconds:()=>Pg,utcSunday:()=>qg,utcSundays:()=>Zg,utcThursday:()=>Vg,utcThursdays:()=>tm,utcTuesday:()=>$g,utcTuesdays:()=>Kg,utcWednesday:()=>Wg,utcWednesdays:()=>Jg,utcWeek:()=>qg,utcWeeks:()=>Zg,utcYear:()=>sm,utcYears:()=>cm,values:()=>da,variance:()=>y,version:()=>r,voronoi:()=>Kk,window:()=>Ot,xml:()=>Ns,zip:()=>W,zoom:()=>fT,zoomIdentity:()=>nT,zoomTransform:()=>rT});var r="5.16.0";function i(t,e){return te?1:t>=e?0:NaN}function a(t){var e;return 1===t.length&&(e=t,t=function(t,n){return i(e(t),n)}),{left:function(e,n,r,i){for(null==r&&(r=0),null==i&&(i=e.length);r>>1;t(e[a],n)<0?r=a+1:i=a}return r},right:function(e,n,r,i){for(null==r&&(r=0),null==i&&(i=e.length);r>>1;t(e[a],n)>0?i=a:r=a+1}return r}}}var o=a(i),s=o.right,c=o.left;const u=s;function l(t,e){null==e&&(e=h);for(var n=0,r=t.length-1,i=t[0],a=new Array(r<0?0:r);nt?1:e>=t?0:NaN}function p(t){return null===t?NaN:+t}function y(t,e){var n,r,i=t.length,a=0,o=-1,s=0,c=0;if(null==e)for(;++o1)return c/(a-1)}function g(t,e){var n=y(t,e);return n?Math.sqrt(n):n}function m(t,e){var n,r,i,a=t.length,o=-1;if(null==e){for(;++o=n)for(r=i=n;++on&&(r=n),i=n)for(r=i=n;++on&&(r=n),i0)return[t];if((r=e0)for(t=Math.ceil(t/o),e=Math.floor(e/o),a=new Array(i=Math.ceil(e-t+1));++s=0?(a>=T?10:a>=E?5:a>=C?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(a>=T?10:a>=E?5:a>=C?2:1)}function M(t,e,n){var r=Math.abs(e-t)/Math.max(0,n),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),a=r/i;return a>=T?i*=10:a>=E?i*=5:a>=C&&(i*=2),eh;)f.pop(),--d;var p,y=new Array(d+1);for(i=0;i<=d;++i)(p=y[i]=[]).x0=i>0?f[i-1]:l,p.x1=i=1)return+n(t[r-1],r-1,t);var r,i=(r-1)*e,a=Math.floor(i),o=+n(t[a],a,t);return o+(+n(t[a+1],a+1,t)-o)*(i-a)}}function B(t,e,n){return t=_.call(t,p).sort(i),Math.ceil((n-e)/(2*(O(t,.75)-O(t,.25))*Math.pow(t.length,-1/3)))}function L(t,e,n){return Math.ceil((n-e)/(3.5*g(t)*Math.pow(t.length,-1/3)))}function I(t,e){var n,r,i=t.length,a=-1;if(null==e){for(;++a=n)for(r=n;++ar&&(r=n)}else for(;++a=n)for(r=n;++ar&&(r=n);return r}function R(t,e){var n,r=t.length,i=r,a=-1,o=0;if(null==e)for(;++a=0;)for(e=(r=t[i]).length;--e>=0;)n[--o]=r[e];return n}function j(t,e){var n,r,i=t.length,a=-1;if(null==e){for(;++a=n)for(r=n;++an&&(r=n)}else for(;++a=n)for(r=n;++an&&(r=n);return r}function Y(t,e){for(var n=e.length,r=new Array(n);n--;)r[n]=t[e[n]];return r}function z(t,e){if(n=t.length){var n,r,a=0,o=0,s=t[o];for(null==e&&(e=i);++a=0&&(n=t.slice(r+1),t=t.slice(0,r)),t&&!e.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))}function lt(t,e){for(var n,r=0,i=t.length;r0)for(var n,r,i=new Array(n),a=0;ae?1:t>=e?0:NaN}bt.prototype={constructor:bt,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,e){return this._parent.insertBefore(t,e)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var kt="http://www.w3.org/1999/xhtml";const Tt={svg:"http://www.w3.org/2000/svg",xhtml:kt,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function Et(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),Tt.hasOwnProperty(e)?{space:Tt[e],local:t}:t}function Ct(t){return function(){this.removeAttribute(t)}}function St(t){return function(){this.removeAttributeNS(t.space,t.local)}}function At(t,e){return function(){this.setAttribute(t,e)}}function Mt(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function Nt(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function Dt(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function Ot(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function Bt(t){return function(){this.style.removeProperty(t)}}function Lt(t,e,n){return function(){this.style.setProperty(t,e,n)}}function It(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function Rt(t,e){return t.style.getPropertyValue(e)||Ot(t).getComputedStyle(t,null).getPropertyValue(e)}function Ft(t){return function(){delete this[t]}}function Pt(t,e){return function(){this[t]=e}}function jt(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function Yt(t){return t.trim().split(/^|\s+/)}function zt(t){return t.classList||new Ut(t)}function Ut(t){this._node=t,this._names=Yt(t.getAttribute("class")||"")}function qt(t,e){for(var n=zt(t),r=-1,i=e.length;++r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var ue={},le=null;function he(t,e,n){return t=fe(t,e,n),function(e){var n=e.relatedTarget;n&&(n===this||8&n.compareDocumentPosition(this))||t.call(this,e)}}function fe(t,e,n){return function(r){var i=le;le=r;try{t.call(this,this.__data__,e,n)}finally{le=i}}}function de(t){return t.trim().split(/^|\s+/).map((function(t){var e="",n=t.indexOf(".");return n>=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}function pe(t){return function(){var e=this.__on;if(e){for(var n,r=0,i=-1,a=e.length;r=x&&(x=_+1);!(b=m[x])&&++x=0;)(r=i[a])&&(o&&4^r.compareDocumentPosition(o)&&o.parentNode.insertBefore(r,o),o=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=wt);for(var n=this._groups,r=n.length,i=new Array(r),a=0;a1?this.each((null==e?Bt:"function"==typeof e?It:Lt)(t,e,null==n?"":n)):Rt(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?Ft:"function"==typeof e?jt:Pt)(t,e)):this.node()[t]},classed:function(t,e){var n=Yt(t+"");if(arguments.length<2){for(var r=zt(this.node()),i=-1,a=n.length;++i>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?Xe(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?Xe(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Pe.exec(t))?new Ke(e[1],e[2],e[3],1):(e=je.exec(t))?new Ke(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Ye.exec(t))?Xe(e[1],e[2],e[3],e[4]):(e=ze.exec(t))?Xe(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Ue.exec(t))?nn(e[1],e[2]/100,e[3]/100,1):(e=qe.exec(t))?nn(e[1],e[2]/100,e[3]/100,e[4]):He.hasOwnProperty(t)?Ge(He[t]):"transparent"===t?new Ke(NaN,NaN,NaN,0):null}function Ge(t){return new Ke(t>>16&255,t>>8&255,255&t,1)}function Xe(t,e,n,r){return r<=0&&(t=e=n=NaN),new Ke(t,e,n,r)}function Ze(t){return t instanceof De||(t=Ve(t)),t?new Ke((t=t.rgb()).r,t.g,t.b,t.opacity):new Ke}function Qe(t,e,n,r){return 1===arguments.length?Ze(t):new Ke(t,e,n,null==r?1:r)}function Ke(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function Je(){return"#"+en(this.r)+en(this.g)+en(this.b)}function tn(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function en(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function nn(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new on(t,e,n,r)}function rn(t){if(t instanceof on)return new on(t.h,t.s,t.l,t.opacity);if(t instanceof De||(t=Ve(t)),!t)return new on;if(t instanceof on)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),a=Math.max(e,n,r),o=NaN,s=a-i,c=(a+i)/2;return s?(o=e===a?(n-r)/s+6*(n0&&c<1?0:o,new on(o,s,c,t.opacity)}function an(t,e,n,r){return 1===arguments.length?rn(t):new on(t,e,n,null==r?1:r)}function on(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function sn(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}function cn(t,e,n,r,i){var a=t*t,o=a*t;return((1-3*t+3*a-o)*e+(4-6*a+3*o)*n+(1+3*t+3*a-3*o)*r+o*i)/6}function un(t){var e=t.length-1;return function(n){var r=n<=0?n=0:n>=1?(n=1,e-1):Math.floor(n*e),i=t[r],a=t[r+1],o=r>0?t[r-1]:2*i-a,s=r180||n<-180?n-360*Math.round(n/360):n):hn(isNaN(t)?e:t)}function pn(t,e){var n=e-t;return n?fn(t,n):hn(isNaN(t)?e:t)}Me(De,Ve,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:$e,formatHex:$e,formatHsl:function(){return rn(this).formatHsl()},formatRgb:We,toString:We}),Me(Ke,Qe,Ne(De,{brighter:function(t){return t=null==t?Be:Math.pow(Be,t),new Ke(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?Oe:Math.pow(Oe,t),new Ke(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Je,formatHex:Je,formatRgb:tn,toString:tn})),Me(on,an,Ne(De,{brighter:function(t){return t=null==t?Be:Math.pow(Be,t),new on(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Oe:Math.pow(Oe,t),new on(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new Ke(sn(t>=240?t-240:t+120,i,r),sn(t,i,r),sn(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));const yn=function t(e){var n=function(t){return 1==(t=+t)?pn:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):hn(isNaN(e)?n:e)}}(e);function r(t,e){var r=n((t=Qe(t)).r,(e=Qe(e)).r),i=n(t.g,e.g),a=n(t.b,e.b),o=pn(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=a(e),t.opacity=o(e),t+""}}return r.gamma=t,r}(1);function gn(t){return function(e){var n,r,i=e.length,a=new Array(i),o=new Array(i),s=new Array(i);for(n=0;na&&(i=e.slice(a,i),s[o]?s[o]+=i:s[++o]=i),(n=n[0])===(r=r[0])?s[o]?s[o]+=r:s[++o]=r:(s[++o]=null,c.push({i:o,x:Tn(n,r)})),a=Sn.lastIndex;return a=0&&e._call.call(null,t),e=e._next;--Rn}function Xn(){Yn=(jn=Un.now())+zn,Rn=Fn=0;try{Gn()}finally{Rn=0,function(){for(var t,e,n=Ln,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:Ln=e);In=t,Qn(r)}(),Yn=0}}function Zn(){var t=Un.now(),e=t-jn;e>1e3&&(zn-=e,jn=t)}function Qn(t){Rn||(Fn&&(Fn=clearTimeout(Fn)),t-Yn>24?(t<1/0&&(Fn=setTimeout(Xn,t-Un.now()-zn)),Pn&&(Pn=clearInterval(Pn))):(Pn||(jn=Un.now(),Pn=setInterval(Zn,1e3)),Rn=1,qn(Xn)))}function Kn(t,e,n){var r=new Wn;return e=null==e?0:+e,r.restart((function(n){r.stop(),t(n+e)}),e,n),r}Wn.prototype=Vn.prototype={constructor:Wn,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?Hn():+n)+(null==e?0:+e),this._next||In===this||(In?In._next=this:Ln=this,In=this),this._call=t,this._time=n,Qn()},stop:function(){this._call&&(this._call=null,this._time=1/0,Qn())}};var Jn=ft("start","end","cancel","interrupt"),tr=[];function er(t,e,n,r,i,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function a(c){var u,l,h,f;if(1!==n.state)return s();for(u in i)if((f=i[u]).name===n.name){if(3===f.state)return Kn(a);4===f.state?(f.state=6,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+u0)throw new Error("too late; already scheduled");return n}function rr(t,e){var n=ir(t,e);if(n.state>3)throw new Error("too late; already running");return n}function ir(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function ar(t,e){var n,r,i,a=t.__transition,o=!0;if(a){for(i in e=null==e?null:e+"",a)(n=a[i]).name===e?(r=n.state>2&&n.state<5,n.state=6,n.timer.stop(),n.on.call(r?"interrupt":"cancel",t,t.__data__,n.index,n.group),delete a[i]):o=!1;o&&delete t.__transition}}var or,sr,cr,ur,lr=180/Math.PI,hr={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function fr(t,e,n,r,i,a){var o,s,c;return(o=Math.sqrt(t*t+e*e))&&(t/=o,e/=o),(c=t*n+e*r)&&(n-=t*c,r-=e*c),(s=Math.sqrt(n*n+r*r))&&(n/=s,r/=s,c/=s),t*r180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(i(n)+"rotate(",null,r)-2,x:Tn(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(a.rotate,o.rotate,s,c),function(t,e,n,a){t!==e?a.push({i:n.push(i(n)+"skewX(",null,r)-2,x:Tn(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(a.skewX,o.skewX,s,c),function(t,e,n,r,a,o){if(t!==n||e!==r){var s=a.push(i(a)+"scale(",null,",",null,")");o.push({i:s-4,x:Tn(t,n)},{i:s-2,x:Tn(e,r)})}else 1===n&&1===r||a.push(i(a)+"scale("+n+","+r+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,c),a=o=null,function(t){for(var e,n=-1,r=c.length;++n=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?nr:rr;return function(){var o=a(this,t),s=o.on;s!==r&&(i=(r=s).copy()).on(e,n),o.on=i}}var Rr=ke.prototype.constructor;function Fr(t){return function(){this.style.removeProperty(t)}}function Pr(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}function jr(t,e,n){var r,i;function a(){var a=e.apply(this,arguments);return a!==i&&(r=(i=a)&&Pr(t,a,n)),r}return a._value=e,a}function Yr(t){return function(e){this.textContent=t.call(this,e)}}function zr(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&Yr(r)),e}return r._value=t,r}var Ur=0;function qr(t,e,n,r){this._groups=t,this._parents=e,this._name=n,this._id=r}function Hr(t){return ke().transition(t)}function $r(){return++Ur}var Wr=ke.prototype;function Vr(t){return t*t*t}function Gr(t){return--t*t*t+1}function Xr(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}qr.prototype=Hr.prototype={constructor:qr,select:function(t){var e=this._name,n=this._id;"function"!=typeof t&&(t=pt(t));for(var r=this._groups,i=r.length,a=new Array(i),o=0;o1&&n.name===e)return new qr([[t]],Kr,e,+r);return null}function ti(t){return function(){return t}}function ei(t,e,n){this.target=t,this.type=e,this.selection=n}function ni(){le.stopImmediatePropagation()}function ri(){le.preventDefault(),le.stopImmediatePropagation()}var ii={name:"drag"},ai={name:"space"},oi={name:"handle"},si={name:"center"};function ci(t){return[+t[0],+t[1]]}function ui(t){return[ci(t[0]),ci(t[1])]}function li(t){return function(e){return On(e,le.touches,t)}}var hi={name:"x",handles:["w","e"].map(bi),input:function(t,e){return null==t?null:[[+t[0],e[0][1]],[+t[1],e[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},fi={name:"y",handles:["n","s"].map(bi),input:function(t,e){return null==t?null:[[e[0][0],+t[0]],[e[1][0],+t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},di={name:"xy",handles:["n","w","e","s","nw","ne","sw","se"].map(bi),input:function(t){return null==t?null:ui(t)},output:function(t){return t}},pi={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},yi={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},gi={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},mi={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},vi={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1};function bi(t){return{type:t}}function _i(){return!le.ctrlKey&&!le.button}function xi(){var t=this.ownerSVGElement||this;return t.hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}function wi(){return navigator.maxTouchPoints||"ontouchstart"in this}function ki(t){for(;!t.__brush;)if(!(t=t.parentNode))return;return t.__brush}function Ti(t){return t[0][0]===t[1][0]||t[0][1]===t[1][1]}function Ei(t){var e=t.__brush;return e?e.dim.output(e.selection):null}function Ci(){return Mi(hi)}function Si(){return Mi(fi)}function Ai(){return Mi(di)}function Mi(t){var e,n=xi,r=_i,i=wi,a=!0,o=ft("start","brush","end"),s=6;function c(e){var n=e.property("__brush",y).selectAll(".overlay").data([bi("overlay")]);n.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",pi.overlay).merge(n).each((function(){var t=ki(this).extent;Te(this).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1])})),e.selectAll(".selection").data([bi("selection")]).enter().append("rect").attr("class","selection").attr("cursor",pi.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var r=e.selectAll(".handle").data(t.handles,(function(t){return t.type}));r.exit().remove(),r.enter().append("rect").attr("class",(function(t){return"handle handle--"+t.type})).attr("cursor",(function(t){return pi[t.type]})),e.each(u).attr("fill","none").attr("pointer-events","all").on("mousedown.brush",f).filter(i).on("touchstart.brush",f).on("touchmove.brush",d).on("touchend.brush touchcancel.brush",p).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function u(){var t=Te(this),e=ki(this).selection;e?(t.selectAll(".selection").style("display",null).attr("x",e[0][0]).attr("y",e[0][1]).attr("width",e[1][0]-e[0][0]).attr("height",e[1][1]-e[0][1]),t.selectAll(".handle").style("display",null).attr("x",(function(t){return"e"===t.type[t.type.length-1]?e[1][0]-s/2:e[0][0]-s/2})).attr("y",(function(t){return"s"===t.type[0]?e[1][1]-s/2:e[0][1]-s/2})).attr("width",(function(t){return"n"===t.type||"s"===t.type?e[1][0]-e[0][0]+s:s})).attr("height",(function(t){return"e"===t.type||"w"===t.type?e[1][1]-e[0][1]+s:s}))):t.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function l(t,e,n){var r=t.__brush.emitter;return!r||n&&r.clean?new h(t,e,n):r}function h(t,e,n){this.that=t,this.args=e,this.state=t.__brush,this.active=0,this.clean=n}function f(){if((!e||le.touches)&&r.apply(this,arguments)){var n,i,o,s,c,h,f,d,p,y,g,m=this,v=le.target.__data__.type,b="selection"===(a&&le.metaKey?v="overlay":v)?ii:a&&le.altKey?si:oi,_=t===fi?null:mi[v],x=t===hi?null:vi[v],w=ki(m),k=w.extent,T=w.selection,E=k[0][0],C=k[0][1],S=k[1][0],A=k[1][1],M=0,N=0,D=_&&x&&a&&le.shiftKey,O=le.touches?li(le.changedTouches[0].identifier):Bn,B=O(m),L=B,I=l(m,arguments,!0).beforestart();"overlay"===v?(T&&(p=!0),w.selection=T=[[n=t===fi?E:B[0],o=t===hi?C:B[1]],[c=t===fi?S:n,f=t===hi?A:o]]):(n=T[0][0],o=T[0][1],c=T[1][0],f=T[1][1]),i=n,s=o,h=c,d=f;var R=Te(m).attr("pointer-events","none"),F=R.selectAll(".overlay").attr("cursor",pi[v]);if(le.touches)I.moved=j,I.ended=z;else{var P=Te(le.view).on("mousemove.brush",j,!0).on("mouseup.brush",z,!0);a&&P.on("keydown.brush",U,!0).on("keyup.brush",q,!0),Se(le.view)}ni(),ar(m),u.call(m),I.start()}function j(){var t=O(m);!D||y||g||(Math.abs(t[0]-L[0])>Math.abs(t[1]-L[1])?g=!0:y=!0),L=t,p=!0,ri(),Y()}function Y(){var t;switch(M=L[0]-B[0],N=L[1]-B[1],b){case ai:case ii:_&&(M=Math.max(E-n,Math.min(S-c,M)),i=n+M,h=c+M),x&&(N=Math.max(C-o,Math.min(A-f,N)),s=o+N,d=f+N);break;case oi:_<0?(M=Math.max(E-n,Math.min(S-n,M)),i=n+M,h=c):_>0&&(M=Math.max(E-c,Math.min(S-c,M)),i=n,h=c+M),x<0?(N=Math.max(C-o,Math.min(A-o,N)),s=o+N,d=f):x>0&&(N=Math.max(C-f,Math.min(A-f,N)),s=o,d=f+N);break;case si:_&&(i=Math.max(E,Math.min(S,n-M*_)),h=Math.max(E,Math.min(S,c+M*_))),x&&(s=Math.max(C,Math.min(A,o-N*x)),d=Math.max(C,Math.min(A,f+N*x)))}h0&&(n=i-M),x<0?f=d-N:x>0&&(o=s-N),b=ai,F.attr("cursor",pi.selection),Y());break;default:return}ri()}function q(){switch(le.keyCode){case 16:D&&(y=g=D=!1,Y());break;case 18:b===si&&(_<0?c=h:_>0&&(n=i),x<0?f=d:x>0&&(o=s),b=oi,Y());break;case 32:b===ai&&(le.altKey?(_&&(c=h-M*_,n=i+M*_),x&&(f=d-N*x,o=s+N*x),b=si):(_<0?c=h:_>0&&(n=i),x<0?f=d:x>0&&(o=s),b=oi),F.attr("cursor",pi[v]),Y());break;default:return}ri()}}function d(){l(this,arguments).moved()}function p(){l(this,arguments).ended()}function y(){var e=this.__brush||{selection:null};return e.extent=ui(n.apply(this,arguments)),e.dim=t,e}return c.move=function(e,n){e.selection?e.on("start.brush",(function(){l(this,arguments).beforestart().start()})).on("interrupt.brush end.brush",(function(){l(this,arguments).end()})).tween("brush",(function(){var e=this,r=e.__brush,i=l(e,arguments),a=r.selection,o=t.input("function"==typeof n?n.apply(this,arguments):n,r.extent),s=Mn(a,o);function c(t){r.selection=1===t&&null===o?null:s(t),u.call(e),i.brush()}return null!==a&&null!==o?c:c(1)})):e.each((function(){var e=this,r=arguments,i=e.__brush,a=t.input("function"==typeof n?n.apply(e,r):n,i.extent),o=l(e,r).beforestart();ar(e),i.selection=null===a?null:a,u.call(e),o.start().brush().end()}))},c.clear=function(t){c.move(t,null)},h.prototype={beforestart:function(){return 1==++this.active&&(this.state.emitter=this,this.starting=!0),this},start:function(){return this.starting?(this.starting=!1,this.emit("start")):this.emit("brush"),this},brush:function(){return this.emit("brush"),this},end:function(){return 0==--this.active&&(delete this.state.emitter,this.emit("end")),this},emit:function(e){ge(new ei(c,e,t.output(this.state.selection)),o.apply,o,[e,this.that,this.args])}},c.extent=function(t){return arguments.length?(n="function"==typeof t?t:ti(ui(t)),c):n},c.filter=function(t){return arguments.length?(r="function"==typeof t?t:ti(!!t),c):r},c.touchable=function(t){return arguments.length?(i="function"==typeof t?t:ti(!!t),c):i},c.handleSize=function(t){return arguments.length?(s=+t,c):s},c.keyModifiers=function(t){return arguments.length?(a=!!t,c):a},c.on=function(){var t=o.on.apply(o,arguments);return t===o?c:t},c}var Ni=Math.cos,Di=Math.sin,Oi=Math.PI,Bi=Oi/2,Li=2*Oi,Ii=Math.max;function Ri(t){return function(e,n){return t(e.source.value+e.target.value,n.source.value+n.target.value)}}function Fi(){var t=0,e=null,n=null,r=null;function i(i){var a,o,s,c,u,l,h=i.length,f=[],d=k(h),p=[],y=[],g=y.groups=new Array(h),m=new Array(h*h);for(a=0,u=-1;++uUi)if(Math.abs(l*s-c*u)>Ui&&i){var f=n-a,d=r-o,p=s*s+c*c,y=f*f+d*d,g=Math.sqrt(p),m=Math.sqrt(h),v=i*Math.tan((Yi-Math.acos((p+h-y)/(2*g*m)))/2),b=v/m,_=v/g;Math.abs(b-1)>Ui&&(this._+="L"+(t+b*u)+","+(e+b*l)),this._+="A"+i+","+i+",0,0,"+ +(l*f>u*d)+","+(this._x1=t+_*s)+","+(this._y1=e+_*c)}else this._+="L"+(this._x1=t)+","+(this._y1=e)},arc:function(t,e,n,r,i,a){t=+t,e=+e,a=!!a;var o=(n=+n)*Math.cos(r),s=n*Math.sin(r),c=t+o,u=e+s,l=1^a,h=a?r-i:i-r;if(n<0)throw new Error("negative radius: "+n);null===this._x1?this._+="M"+c+","+u:(Math.abs(this._x1-c)>Ui||Math.abs(this._y1-u)>Ui)&&(this._+="L"+c+","+u),n&&(h<0&&(h=h%zi+zi),h>qi?this._+="A"+n+","+n+",0,1,"+l+","+(t-o)+","+(e-s)+"A"+n+","+n+",0,1,"+l+","+(this._x1=c)+","+(this._y1=u):h>Ui&&(this._+="A"+n+","+n+",0,"+ +(h>=Yi)+","+l+","+(this._x1=t+n*Math.cos(i))+","+(this._y1=e+n*Math.sin(i))))},rect:function(t,e,n,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+e)+"h"+ +n+"v"+ +r+"h"+-n+"Z"},toString:function(){return this._}};const Wi=$i;function Vi(t){return t.source}function Gi(t){return t.target}function Xi(t){return t.radius}function Zi(t){return t.startAngle}function Qi(t){return t.endAngle}function Ki(){var t=Vi,e=Gi,n=Xi,r=Zi,i=Qi,a=null;function o(){var o,s=Pi.call(arguments),c=t.apply(this,s),u=e.apply(this,s),l=+n.apply(this,(s[0]=c,s)),h=r.apply(this,s)-Bi,f=i.apply(this,s)-Bi,d=l*Ni(h),p=l*Di(h),y=+n.apply(this,(s[0]=u,s)),g=r.apply(this,s)-Bi,m=i.apply(this,s)-Bi;if(a||(a=o=Wi()),a.moveTo(d,p),a.arc(0,0,l,h,f),h===g&&f===m||(a.quadraticCurveTo(0,0,y*Ni(g),y*Di(g)),a.arc(0,0,y,g,m)),a.quadraticCurveTo(0,0,d,p),a.closePath(),o)return a=null,o+""||null}return o.radius=function(t){return arguments.length?(n="function"==typeof t?t:ji(+t),o):n},o.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:ji(+t),o):r},o.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:ji(+t),o):i},o.source=function(e){return arguments.length?(t=e,o):t},o.target=function(t){return arguments.length?(e=t,o):e},o.context=function(t){return arguments.length?(a=null==t?null:t,o):a},o}var Ji="$";function ta(){}function ea(t,e){var n=new ta;if(t instanceof ta)t.each((function(t,e){n.set(e,t)}));else if(Array.isArray(t)){var r,i=-1,a=t.length;if(null==e)for(;++i=r.length)return null!=t&&n.sort(t),null!=e?e(n):n;for(var c,u,l,h=-1,f=n.length,d=r[i++],p=na(),y=o();++hr.length)return t;var a,s=i[n-1];return null!=e&&n>=r.length?a=t.entries():(a=[],t.each((function(t,e){a.push({key:e,values:o(t,n)})}))),null!=s?a.sort((function(t,e){return s(t.key,e.key)})):a}return n={object:function(t){return a(t,0,ia,aa)},map:function(t){return a(t,0,oa,sa)},entries:function(t){return o(a(t,0,oa,sa),0)},key:function(t){return r.push(t),n},sortKeys:function(t){return i[r.length-1]=t,n},sortValues:function(e){return t=e,n},rollup:function(t){return e=t,n}}}function ia(){return{}}function aa(t,e,n){t[e]=n}function oa(){return na()}function sa(t,e,n){t.set(e,n)}function ca(){}var ua=na.prototype;function la(t,e){var n=new ca;if(t instanceof ca)t.each((function(t){n.add(t)}));else if(t){var r=-1,i=t.length;if(null==e)for(;++r.008856451679035631?Math.pow(t,1/3):t/xa+ba}function Sa(t){return t>_a?t*t*t:xa*(t-ba)}function Aa(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Ma(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Na(t){if(t instanceof Ba)return new Ba(t.h,t.c,t.l,t.opacity);if(t instanceof Ea||(t=wa(t)),0===t.a&&0===t.b)return new Ba(NaN,0r!=d>r&&n<(f-u)*(r-l)/(d-l)+u&&(i=-i)}return i}function Qa(t,e,n){var r,i,a,o;return function(t,e,n){return(e[0]-t[0])*(n[1]-t[1])==(n[0]-t[0])*(e[1]-t[1])}(t,e,n)&&(i=t[r=+(t[0]===e[0])],a=n[r],o=e[r],i<=a&&a<=o||o<=a&&a<=i)}function Ka(){}var Ja=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function to(){var t=1,e=1,n=N,r=s;function i(t){var e=n(t);if(Array.isArray(e))e=e.slice().sort(Va);else{var r=m(t),i=r[0],o=r[1];e=M(i,o,e),e=k(Math.floor(i/e)*e,Math.floor(o/e)*e,e)}return e.map((function(e){return a(t,e)}))}function a(n,i){var a=[],s=[];return function(n,r,i){var a,s,c,u,l,h,f=new Array,d=new Array;for(a=s=-1,u=n[0]>=r,Ja[u<<1].forEach(p);++a=r,Ja[c|u<<1].forEach(p);for(Ja[u<<0].forEach(p);++s=r,l=n[s*t]>=r,Ja[u<<1|l<<2].forEach(p);++a=r,h=l,l=n[s*t+a+1]>=r,Ja[c|u<<1|l<<2|h<<3].forEach(p);Ja[u|l<<3].forEach(p)}for(a=-1,l=n[s*t]>=r,Ja[l<<2].forEach(p);++a=r,Ja[l<<2|h<<3].forEach(p);function p(t){var e,n,r=[t[0][0]+a,t[0][1]+s],c=[t[1][0]+a,t[1][1]+s],u=o(r),l=o(c);(e=d[u])?(n=f[l])?(delete d[e.end],delete f[n.start],e===n?(e.ring.push(c),i(e.ring)):f[e.start]=d[n.end]={start:e.start,end:n.end,ring:e.ring.concat(n.ring)}):(delete d[e.end],e.ring.push(c),d[e.end=l]=e):(e=f[l])?(n=d[u])?(delete f[e.start],delete d[n.end],e===n?(e.ring.push(c),i(e.ring)):f[n.start]=d[e.end]={start:n.start,end:e.end,ring:n.ring.concat(e.ring)}):(delete f[e.start],e.ring.unshift(r),f[e.start=u]=e):f[u]=d[l]={start:u,end:l,ring:[r,c]}}Ja[l<<3].forEach(p)}(n,i,(function(t){r(t,n,i),function(t){for(var e=0,n=t.length,r=t[n-1][1]*t[0][0]-t[n-1][0]*t[0][1];++e0?a.push([t]):s.push(t)})),s.forEach((function(t){for(var e,n=0,r=a.length;n0&&o0&&s0&&a>0))throw new Error("invalid size");return t=r,e=a,i},i.thresholds=function(t){return arguments.length?(n="function"==typeof t?t:Array.isArray(t)?Ga(Wa.call(t)):Ga(t),i):n},i.smooth=function(t){return arguments.length?(r=t?s:Ka,i):r===s},i}function eo(t,e,n){for(var r=t.width,i=t.height,a=1+(n<<1),o=0;o=n&&(s>=a&&(c-=t.data[s-a+o*r]),e.data[s-n+o*r]=c/Math.min(s+1,r-1+a-s,a))}function no(t,e,n){for(var r=t.width,i=t.height,a=1+(n<<1),o=0;o=n&&(s>=a&&(c-=t.data[o+(s-a)*r]),e.data[o+(s-n)*r]=c/Math.min(s+1,i-1+a-s,a))}function ro(t){return t[0]}function io(t){return t[1]}function ao(){return 1}function oo(){var t=ro,e=io,n=ao,r=960,i=500,a=20,o=2,s=3*a,c=r+2*s>>o,u=i+2*s>>o,l=Ga(20);function h(r){var i=new Float32Array(c*u),h=new Float32Array(c*u);r.forEach((function(r,a,l){var h=+t(r,a,l)+s>>o,f=+e(r,a,l)+s>>o,d=+n(r,a,l);h>=0&&h=0&&f>o),no({width:c,height:u,data:h},{width:c,height:u,data:i},a>>o),eo({width:c,height:u,data:i},{width:c,height:u,data:h},a>>o),no({width:c,height:u,data:h},{width:c,height:u,data:i},a>>o),eo({width:c,height:u,data:i},{width:c,height:u,data:h},a>>o),no({width:c,height:u,data:h},{width:c,height:u,data:i},a>>o);var d=l(i);if(!Array.isArray(d)){var p=I(i);d=M(0,p,d),(d=k(0,Math.floor(p/d)*d,d)).shift()}return to().thresholds(d).size([c,u])(i).map(f)}function f(t){return t.value*=Math.pow(2,-2*o),t.coordinates.forEach(d),t}function d(t){t.forEach(p)}function p(t){t.forEach(y)}function y(t){t[0]=t[0]*Math.pow(2,o)-s,t[1]=t[1]*Math.pow(2,o)-s}function g(){return c=r+2*(s=3*a)>>o,u=i+2*s>>o,h}return h.x=function(e){return arguments.length?(t="function"==typeof e?e:Ga(+e),h):t},h.y=function(t){return arguments.length?(e="function"==typeof t?t:Ga(+t),h):e},h.weight=function(t){return arguments.length?(n="function"==typeof t?t:Ga(+t),h):n},h.size=function(t){if(!arguments.length)return[r,i];var e=Math.ceil(t[0]),n=Math.ceil(t[1]);if(!(e>=0||e>=0))throw new Error("invalid size");return r=e,i=n,g()},h.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return o=Math.floor(Math.log(t)/Math.LN2),g()},h.thresholds=function(t){return arguments.length?(l="function"==typeof t?t:Array.isArray(t)?Ga(Wa.call(t)):Ga(t),h):l},h.bandwidth=function(t){if(!arguments.length)return Math.sqrt(a*(a+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return a=Math.round((Math.sqrt(4*t*t+1)-1)/2),g()},h}function so(t){return function(){return t}}function co(t,e,n,r,i,a,o,s,c,u){this.target=t,this.type=e,this.subject=n,this.identifier=r,this.active=i,this.x=a,this.y=o,this.dx=s,this.dy=c,this._=u}function uo(){return!le.ctrlKey&&!le.button}function lo(){return this.parentNode}function ho(t){return null==t?{x:le.x,y:le.y}:t}function fo(){return navigator.maxTouchPoints||"ontouchstart"in this}function po(){var t,e,n,r,i=uo,a=lo,o=ho,s=fo,c={},u=ft("start","drag","end"),l=0,h=0;function f(t){t.on("mousedown.drag",d).filter(s).on("touchstart.drag",g).on("touchmove.drag",m).on("touchend.drag touchcancel.drag",v).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function d(){if(!r&&i.apply(this,arguments)){var o=b("mouse",a.apply(this,arguments),Bn,this,arguments);o&&(Te(le.view).on("mousemove.drag",p,!0).on("mouseup.drag",y,!0),Se(le.view),Ee(),n=!1,t=le.clientX,e=le.clientY,o("start"))}}function p(){if(Ce(),!n){var r=le.clientX-t,i=le.clientY-e;n=r*r+i*i>h}c.mouse("drag")}function y(){Te(le.view).on("mousemove.drag mouseup.drag",null),Ae(le.view,n),Ce(),c.mouse("end")}function g(){if(i.apply(this,arguments)){var t,e,n=le.changedTouches,r=a.apply(this,arguments),o=n.length;for(t=0;t=a?c=!0:10===(r=t.charCodeAt(o++))?u=!0:13===r&&(u=!0,10===t.charCodeAt(o)&&++o),t.slice(i+1,e-1).replace(/""/g,'"')}for(;o9999?"+"+bo(t,6):bo(t,4)}(t.getUTCFullYear())+"-"+bo(t.getUTCMonth()+1,2)+"-"+bo(t.getUTCDate(),2)+(i?"T"+bo(e,2)+":"+bo(n,2)+":"+bo(r,2)+"."+bo(i,3)+"Z":r?"T"+bo(e,2)+":"+bo(n,2)+":"+bo(r,2)+"Z":n||e?"T"+bo(e,2)+":"+bo(n,2)+"Z":"")}(t):e.test(t+="")?'"'+t.replace(/"/g,'""')+'"':t}return{parse:function(t,e){var n,i,a=r(t,(function(t,r){if(n)return n(t,r-1);i=t,n=e?function(t,e){var n=mo(t);return function(r,i){return e(n(r),i,t)}}(t,e):mo(t)}));return a.columns=i||[],a},parseRows:r,format:function(e,n){return null==n&&(n=vo(e)),[n.map(o).join(t)].concat(i(e,n)).join("\n")},formatBody:function(t,e){return null==e&&(e=vo(t)),i(t,e).join("\n")},formatRows:function(t){return t.map(a).join("\n")},formatRow:a,formatValue:o}}var xo=_o(","),wo=xo.parse,ko=xo.parseRows,To=xo.format,Eo=xo.formatBody,Co=xo.formatRows,So=xo.formatRow,Ao=xo.formatValue,Mo=_o("\t"),No=Mo.parse,Do=Mo.parseRows,Oo=Mo.format,Bo=Mo.formatBody,Lo=Mo.formatRows,Io=Mo.formatRow,Ro=Mo.formatValue;function Fo(t){for(var e in t){var n,r,i=t[e].trim();if(i)if("true"===i)i=!0;else if("false"===i)i=!1;else if("NaN"===i)i=NaN;else if(isNaN(n=+i)){if(!(r=i.match(/^([-+]\d{2})?\d{4}(-\d{2}(-\d{2})?)?(T\d{2}:\d{2}(:\d{2}(\.\d{3})?)?(Z|[-+]\d{2}:\d{2})?)?$/)))continue;Po&&r[4]&&!r[7]&&(i=i.replace(/-/g,"/").replace(/T/," ")),i=new Date(i)}else i=n;else i=null;t[e]=i}return t}var Po=new Date("2019-01-01T00:00").getHours()||new Date("2019-07-01T00:00").getHours();function jo(t){return+t}function Yo(t){return t*t}function zo(t){return t*(2-t)}function Uo(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}var qo=function t(e){function n(t){return Math.pow(t,e)}return e=+e,n.exponent=t,n}(3),Ho=function t(e){function n(t){return 1-Math.pow(1-t,e)}return e=+e,n.exponent=t,n}(3),$o=function t(e){function n(t){return((t*=2)<=1?Math.pow(t,e):2-Math.pow(2-t,e))/2}return e=+e,n.exponent=t,n}(3),Wo=Math.PI,Vo=Wo/2;function Go(t){return 1==+t?1:1-Math.cos(t*Vo)}function Xo(t){return Math.sin(t*Vo)}function Zo(t){return(1-Math.cos(Wo*t))/2}function Qo(t){return 1.0009775171065494*(Math.pow(2,-10*t)-.0009765625)}function Ko(t){return Qo(1-+t)}function Jo(t){return 1-Qo(t)}function ts(t){return((t*=2)<=1?Qo(1-t):2-Qo(t-1))/2}function es(t){return 1-Math.sqrt(1-t*t)}function ns(t){return Math.sqrt(1- --t*t)}function rs(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}var is=7.5625;function as(t){return 1-os(1-t)}function os(t){return(t=+t)<.36363636363636365?is*t*t:t<.7272727272727273?is*(t-=.5454545454545454)*t+.75:t<.9090909090909091?is*(t-=.8181818181818182)*t+.9375:is*(t-=.9545454545454546)*t+.984375}function ss(t){return((t*=2)<=1?1-os(1-t):os(t-1)+1)/2}var cs=1.70158,us=function t(e){function n(t){return(t=+t)*t*(e*(t-1)+t)}return e=+e,n.overshoot=t,n}(cs),ls=function t(e){function n(t){return--t*t*((t+1)*e+t)+1}return e=+e,n.overshoot=t,n}(cs),hs=function t(e){function n(t){return((t*=2)<1?t*t*((e+1)*t-e):(t-=2)*t*((e+1)*t+e)+2)/2}return e=+e,n.overshoot=t,n}(cs),fs=2*Math.PI,ds=function t(e,n){var r=Math.asin(1/(e=Math.max(1,e)))*(n/=fs);function i(t){return e*Qo(- --t)*Math.sin((r-t)/n)}return i.amplitude=function(e){return t(e,n*fs)},i.period=function(n){return t(e,n)},i}(1,.3),ps=function t(e,n){var r=Math.asin(1/(e=Math.max(1,e)))*(n/=fs);function i(t){return 1-e*Qo(t=+t)*Math.sin((t+r)/n)}return i.amplitude=function(e){return t(e,n*fs)},i.period=function(n){return t(e,n)},i}(1,.3),ys=function t(e,n){var r=Math.asin(1/(e=Math.max(1,e)))*(n/=fs);function i(t){return((t=2*t-1)<0?e*Qo(-t)*Math.sin((r-t)/n):2-e*Qo(t)*Math.sin((r+t)/n))/2}return i.amplitude=function(e){return t(e,n*fs)},i.period=function(n){return t(e,n)},i}(1,.3);function gs(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.blob()}function ms(t,e){return fetch(t,e).then(gs)}function vs(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.arrayBuffer()}function bs(t,e){return fetch(t,e).then(vs)}function _s(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.text()}function xs(t,e){return fetch(t,e).then(_s)}function ws(t){return function(e,n,r){return 2===arguments.length&&"function"==typeof n&&(r=n,n=void 0),xs(e,n).then((function(e){return t(e,r)}))}}function ks(t,e,n,r){3===arguments.length&&"function"==typeof n&&(r=n,n=void 0);var i=_o(t);return xs(e,n).then((function(t){return i.parse(t,r)}))}var Ts=ws(wo),Es=ws(No);function Cs(t,e){return new Promise((function(n,r){var i=new Image;for(var a in e)i[a]=e[a];i.onerror=r,i.onload=function(){n(i)},i.src=t}))}function Ss(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);if(204!==t.status&&205!==t.status)return t.json()}function As(t,e){return fetch(t,e).then(Ss)}function Ms(t){return function(e,n){return xs(e,n).then((function(e){return(new DOMParser).parseFromString(e,t)}))}}const Ns=Ms("application/xml");var Ds=Ms("text/html"),Os=Ms("image/svg+xml");function Bs(t,e){var n;function r(){var r,i,a=n.length,o=0,s=0;for(r=0;r=(a=(y+m)/2))?y=a:m=a,(l=n>=(o=(g+v)/2))?g=o:v=o,i=d,!(d=d[h=l<<1|u]))return i[h]=p,t;if(s=+t._x.call(null,d.data),c=+t._y.call(null,d.data),e===s&&n===c)return p.next=d,i?i[h]=p:t._root=p,t;do{i=i?i[h]=new Array(4):t._root=new Array(4),(u=e>=(a=(y+m)/2))?y=a:m=a,(l=n>=(o=(g+v)/2))?g=o:v=o}while((h=l<<1|u)==(f=(c>=o)<<1|s>=a));return i[f]=d,i[h]=p,t}function Fs(t,e,n,r,i){this.node=t,this.x0=e,this.y0=n,this.x1=r,this.y1=i}function Ps(t){return t[0]}function js(t){return t[1]}function Ys(t,e,n){var r=new zs(null==e?Ps:e,null==n?js:n,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function zs(t,e,n,r,i,a){this._x=t,this._y=e,this._x0=n,this._y0=r,this._x1=i,this._y1=a,this._root=void 0}function Us(t){for(var e={data:t.data},n=e;t=t.next;)n=n.next={data:t.data};return e}var qs=Ys.prototype=zs.prototype;function Hs(t){return t.x+t.vx}function $s(t){return t.y+t.vy}function Ws(t){var e,n,r=1,i=1;function a(){for(var t,a,s,c,u,l,h,f=e.length,d=0;dc+d||iu+d||as.index){var p=c-o.x-o.vx,y=u-o.y-o.vy,g=p*p+y*y;gt.r&&(t.r=t[e].r)}function s(){if(e){var r,i,a=e.length;for(n=new Array(a),r=0;rl&&(l=r),ih&&(h=i));if(c>l||u>h)return this;for(this.cover(c,u).cover(l,h),n=0;nt||t>=i||r>e||e>=a;)switch(s=(ef||(a=c.y0)>d||(o=c.x1)=m)<<1|t>=g)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-u],p[p.length-1-u]=c)}else{var v=t-+this._x.call(null,y.data),b=e-+this._y.call(null,y.data),_=v*v+b*b;if(_=(s=(p+g)/2))?p=s:g=s,(l=o>=(c=(y+m)/2))?y=c:m=c,e=d,!(d=d[h=l<<1|u]))return this;if(!d.length)break;(e[h+1&3]||e[h+2&3]||e[h+3&3])&&(n=e,f=h)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):e?(i?e[h]=i:delete e[h],(d=e[0]||e[1]||e[2]||e[3])&&d===(e[3]||e[2]||e[1]||e[0])&&!d.length&&(n?n[f]=d:this._root=d),this):(this._root=i,this)},qs.removeAll=function(t){for(var e=0,n=t.length;e1?(null==n?s.remove(t):s.set(t,d(n)),e):s.get(t)},find:function(e,n,r){var i,a,o,s,c,u=0,l=t.length;for(null==r?r=1/0:r*=r,u=0;u1?(u.on(t,n),e):u.on(t)}}}function tc(){var t,e,n,r,i=Ls(-30),a=1,o=1/0,s=.81;function c(r){var i,a=t.length,o=Ys(t,Zs,Qs).visitAfter(l);for(n=r,i=0;i=o)){(t.data!==e||t.next)&&(0===l&&(d+=(l=Is())*l),0===h&&(d+=(h=Is())*h),d1?r[0]+r.slice(2):r,+t.slice(n+1)]}function ac(t){return(t=ic(Math.abs(t)))?t[1]:NaN}var oc,sc=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function cc(t){if(!(e=sc.exec(t)))throw new Error("invalid format: "+t);var e;return new uc({fill:e[1],align:e[2],sign:e[3],symbol:e[4],zero:e[5],width:e[6],comma:e[7],precision:e[8]&&e[8].slice(1),trim:e[9],type:e[10]})}function uc(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function lc(t,e){var n=ic(t,e);if(!n)return t+"";var r=n[0],i=n[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}cc.prototype=uc.prototype,uc.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};const hc={"%":function(t,e){return(100*t).toFixed(e)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.abs(t=Math.round(t))>=1e21?t.toLocaleString("en").replace(/,/g,""):t.toString(10)},e:function(t,e){return t.toExponential(e)},f:function(t,e){return t.toFixed(e)},g:function(t,e){return t.toPrecision(e)},o:function(t){return Math.round(t).toString(8)},p:function(t,e){return lc(100*t,e)},r:lc,s:function(t,e){var n=ic(t,e);if(!n)return t+"";var r=n[0],i=n[1],a=i-(oc=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,o=r.length;return a===o?r:a>o?r+new Array(a-o+1).join("0"):a>0?r.slice(0,a)+"."+r.slice(a):"0."+new Array(1-a).join("0")+ic(t,Math.max(0,e+a-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}};function fc(t){return t}var dc,pc,yc,gc=Array.prototype.map,mc=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function vc(t){var e,n,r=void 0===t.grouping||void 0===t.thousands?fc:(e=gc.call(t.grouping,Number),n=t.thousands+"",function(t,r){for(var i=t.length,a=[],o=0,s=e[0],c=0;i>0&&s>0&&(c+s+1>r&&(s=Math.max(1,r-c)),a.push(t.substring(i-=s,i+s)),!((c+=s+1)>r));)s=e[o=(o+1)%e.length];return a.reverse().join(n)}),i=void 0===t.currency?"":t.currency[0]+"",a=void 0===t.currency?"":t.currency[1]+"",o=void 0===t.decimal?".":t.decimal+"",s=void 0===t.numerals?fc:function(t){return function(e){return e.replace(/[0-9]/g,(function(e){return t[+e]}))}}(gc.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",u=void 0===t.minus?"-":t.minus+"",l=void 0===t.nan?"NaN":t.nan+"";function h(t){var e=(t=cc(t)).fill,n=t.align,h=t.sign,f=t.symbol,d=t.zero,p=t.width,y=t.comma,g=t.precision,m=t.trim,v=t.type;"n"===v?(y=!0,v="g"):hc[v]||(void 0===g&&(g=12),m=!0,v="g"),(d||"0"===e&&"="===n)&&(d=!0,e="0",n="=");var b="$"===f?i:"#"===f&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",_="$"===f?a:/[%p]/.test(v)?c:"",x=hc[v],w=/[defgprs%]/.test(v);function k(t){var i,a,c,f=b,k=_;if("c"===v)k=x(t)+k,t="";else{var T=(t=+t)<0||1/t<0;if(t=isNaN(t)?l:x(Math.abs(t),g),m&&(t=function(t){t:for(var e,n=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(e+1):t}(t)),T&&0==+t&&"+"!==h&&(T=!1),f=(T?"("===h?h:u:"-"===h||"("===h?"":h)+f,k=("s"===v?mc[8+oc/3]:"")+k+(T&&"("===h?")":""),w)for(i=-1,a=t.length;++i(c=t.charCodeAt(i))||c>57){k=(46===c?o+t.slice(i+1):t.slice(i))+k,t=t.slice(0,i);break}}y&&!d&&(t=r(t,1/0));var E=f.length+t.length+k.length,C=E>1)+f+t+k+C.slice(E);break;default:t=C+f+t+k}return s(t)}return g=void 0===g?6:/[gprs]/.test(v)?Math.max(1,Math.min(21,g)):Math.max(0,Math.min(20,g)),k.toString=function(){return t+""},k}return{format:h,formatPrefix:function(t,e){var n=h(((t=cc(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(ac(e)/3))),i=Math.pow(10,-r),a=mc[8+r/3];return function(t){return n(i*t)+a}}}}function bc(t){return dc=vc(t),pc=dc.format,yc=dc.formatPrefix,dc}function _c(t){return Math.max(0,-ac(Math.abs(t)))}function xc(t,e){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(ac(e)/3)))-ac(Math.abs(t)))}function wc(t,e){return t=Math.abs(t),e=Math.abs(e)-t,Math.max(0,ac(e)-ac(t))+1}function kc(){return new Tc}function Tc(){this.reset()}bc({decimal:".",thousands:",",grouping:[3],currency:["$",""],minus:"-"}),Tc.prototype={constructor:Tc,reset:function(){this.s=this.t=0},add:function(t){Cc(Ec,t,this.t),Cc(this,Ec.s,this.s),this.s?this.t+=Ec.t:this.s=Ec.t},valueOf:function(){return this.s}};var Ec=new Tc;function Cc(t,e,n){var r=t.s=e+n,i=r-e,a=r-i;t.t=e-a+(n-i)}var Sc=1e-6,Ac=1e-12,Mc=Math.PI,Nc=Mc/2,Dc=Mc/4,Oc=2*Mc,Bc=180/Mc,Lc=Mc/180,Ic=Math.abs,Rc=Math.atan,Fc=Math.atan2,Pc=Math.cos,jc=Math.ceil,Yc=Math.exp,zc=(Math.floor,Math.log),Uc=Math.pow,qc=Math.sin,Hc=Math.sign||function(t){return t>0?1:t<0?-1:0},$c=Math.sqrt,Wc=Math.tan;function Vc(t){return t>1?0:t<-1?Mc:Math.acos(t)}function Gc(t){return t>1?Nc:t<-1?-Nc:Math.asin(t)}function Xc(t){return(t=qc(t/2))*t}function Zc(){}function Qc(t,e){t&&Jc.hasOwnProperty(t.type)&&Jc[t.type](t,e)}var Kc={Feature:function(t,e){Qc(t.geometry,e)},FeatureCollection:function(t,e){for(var n=t.features,r=-1,i=n.length;++r=0?1:-1,i=r*n,a=Pc(e=(e*=Lc)/2+Dc),o=qc(e),s=su*o,c=ou*a+s*Pc(i),u=s*r*qc(i);cu.add(Fc(u,c)),au=t,ou=a,su=o}function yu(t){return uu.reset(),nu(t,lu),2*uu}function gu(t){return[Fc(t[1],t[0]),Gc(t[2])]}function mu(t){var e=t[0],n=t[1],r=Pc(n);return[r*Pc(e),r*qc(e),qc(n)]}function vu(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]}function bu(t,e){return[t[1]*e[2]-t[2]*e[1],t[2]*e[0]-t[0]*e[2],t[0]*e[1]-t[1]*e[0]]}function _u(t,e){t[0]+=e[0],t[1]+=e[1],t[2]+=e[2]}function xu(t,e){return[t[0]*e,t[1]*e,t[2]*e]}function wu(t){var e=$c(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=e,t[1]/=e,t[2]/=e}var ku,Tu,Eu,Cu,Su,Au,Mu,Nu,Du,Ou,Bu,Lu,Iu,Ru,Fu,Pu,ju,Yu,zu,Uu,qu,Hu,$u,Wu,Vu,Gu,Xu=kc(),Zu={point:Qu,lineStart:Ju,lineEnd:tl,polygonStart:function(){Zu.point=el,Zu.lineStart=nl,Zu.lineEnd=rl,Xu.reset(),lu.polygonStart()},polygonEnd:function(){lu.polygonEnd(),Zu.point=Qu,Zu.lineStart=Ju,Zu.lineEnd=tl,cu<0?(ku=-(Eu=180),Tu=-(Cu=90)):Xu>Sc?Cu=90:Xu<-1e-6&&(Tu=-90),Ou[0]=ku,Ou[1]=Eu},sphere:function(){ku=-(Eu=180),Tu=-(Cu=90)}};function Qu(t,e){Du.push(Ou=[ku=t,Eu=t]),eCu&&(Cu=e)}function Ku(t,e){var n=mu([t*Lc,e*Lc]);if(Nu){var r=bu(Nu,n),i=bu([r[1],-r[0],0],r);wu(i),i=gu(i);var a,o=t-Su,s=o>0?1:-1,c=i[0]*Bc*s,u=Ic(o)>180;u^(s*SuCu&&(Cu=a):u^(s*Su<(c=(c+360)%360-180)&&cCu&&(Cu=e)),u?til(ku,Eu)&&(Eu=t):il(t,Eu)>il(ku,Eu)&&(ku=t):Eu>=ku?(tEu&&(Eu=t)):t>Su?il(ku,t)>il(ku,Eu)&&(Eu=t):il(t,Eu)>il(ku,Eu)&&(ku=t)}else Du.push(Ou=[ku=t,Eu=t]);eCu&&(Cu=e),Nu=n,Su=t}function Ju(){Zu.point=Ku}function tl(){Ou[0]=ku,Ou[1]=Eu,Zu.point=Qu,Nu=null}function el(t,e){if(Nu){var n=t-Su;Xu.add(Ic(n)>180?n+(n>0?360:-360):n)}else Au=t,Mu=e;lu.point(t,e),Ku(t,e)}function nl(){lu.lineStart()}function rl(){el(Au,Mu),lu.lineEnd(),Ic(Xu)>Sc&&(ku=-(Eu=180)),Ou[0]=ku,Ou[1]=Eu,Nu=null}function il(t,e){return(e-=t)<0?e+360:e}function al(t,e){return t[0]-e[0]}function ol(t,e){return t[0]<=t[1]?t[0]<=e&&e<=t[1]:eil(r[0],r[1])&&(r[1]=i[1]),il(i[0],r[1])>il(r[0],r[1])&&(r[0]=i[0])):a.push(r=i);for(o=-1/0,e=0,r=a[n=a.length-1];e<=n;r=i,++e)i=a[e],(s=il(r[1],i[0]))>o&&(o=s,ku=i[0],Eu=r[1])}return Du=Ou=null,ku===1/0||Tu===1/0?[[NaN,NaN],[NaN,NaN]]:[[ku,Tu],[Eu,Cu]]}var cl={sphere:Zc,point:ul,lineStart:hl,lineEnd:pl,polygonStart:function(){cl.lineStart=yl,cl.lineEnd=gl},polygonEnd:function(){cl.lineStart=hl,cl.lineEnd=pl}};function ul(t,e){t*=Lc;var n=Pc(e*=Lc);ll(n*Pc(t),n*qc(t),qc(e))}function ll(t,e,n){++Bu,Iu+=(t-Iu)/Bu,Ru+=(e-Ru)/Bu,Fu+=(n-Fu)/Bu}function hl(){cl.point=fl}function fl(t,e){t*=Lc;var n=Pc(e*=Lc);Wu=n*Pc(t),Vu=n*qc(t),Gu=qc(e),cl.point=dl,ll(Wu,Vu,Gu)}function dl(t,e){t*=Lc;var n=Pc(e*=Lc),r=n*Pc(t),i=n*qc(t),a=qc(e),o=Fc($c((o=Vu*a-Gu*i)*o+(o=Gu*r-Wu*a)*o+(o=Wu*i-Vu*r)*o),Wu*r+Vu*i+Gu*a);Lu+=o,Pu+=o*(Wu+(Wu=r)),ju+=o*(Vu+(Vu=i)),Yu+=o*(Gu+(Gu=a)),ll(Wu,Vu,Gu)}function pl(){cl.point=ul}function yl(){cl.point=ml}function gl(){vl(Hu,$u),cl.point=ul}function ml(t,e){Hu=t,$u=e,t*=Lc,e*=Lc,cl.point=vl;var n=Pc(e);Wu=n*Pc(t),Vu=n*qc(t),Gu=qc(e),ll(Wu,Vu,Gu)}function vl(t,e){t*=Lc;var n=Pc(e*=Lc),r=n*Pc(t),i=n*qc(t),a=qc(e),o=Vu*a-Gu*i,s=Gu*r-Wu*a,c=Wu*i-Vu*r,u=$c(o*o+s*s+c*c),l=Gc(u),h=u&&-l/u;zu+=h*o,Uu+=h*s,qu+=h*c,Lu+=l,Pu+=l*(Wu+(Wu=r)),ju+=l*(Vu+(Vu=i)),Yu+=l*(Gu+(Gu=a)),ll(Wu,Vu,Gu)}function bl(t){Bu=Lu=Iu=Ru=Fu=Pu=ju=Yu=zu=Uu=qu=0,nu(t,cl);var e=zu,n=Uu,r=qu,i=e*e+n*n+r*r;return iMc?t+Math.round(-t/Oc)*Oc:t,e]}function kl(t,e,n){return(t%=Oc)?e||n?xl(El(t),Cl(e,n)):El(t):e||n?Cl(e,n):wl}function Tl(t){return function(e,n){return[(e+=t)>Mc?e-Oc:e<-Mc?e+Oc:e,n]}}function El(t){var e=Tl(t);return e.invert=Tl(-t),e}function Cl(t,e){var n=Pc(t),r=qc(t),i=Pc(e),a=qc(e);function o(t,e){var o=Pc(e),s=Pc(t)*o,c=qc(t)*o,u=qc(e),l=u*n+s*r;return[Fc(c*i-l*a,s*n-u*r),Gc(l*i+c*a)]}return o.invert=function(t,e){var o=Pc(e),s=Pc(t)*o,c=qc(t)*o,u=qc(e),l=u*i-c*a;return[Fc(c*i+u*a,s*n+l*r),Gc(l*n-s*r)]},o}function Sl(t){function e(e){return(e=t(e[0]*Lc,e[1]*Lc))[0]*=Bc,e[1]*=Bc,e}return t=kl(t[0]*Lc,t[1]*Lc,t.length>2?t[2]*Lc:0),e.invert=function(e){return(e=t.invert(e[0]*Lc,e[1]*Lc))[0]*=Bc,e[1]*=Bc,e},e}function Al(t,e,n,r,i,a){if(n){var o=Pc(e),s=qc(e),c=r*n;null==i?(i=e+r*Oc,a=e-c/2):(i=Ml(o,i),a=Ml(o,a),(r>0?ia)&&(i+=r*Oc));for(var u,l=i;r>0?l>a:l1&&e.push(e.pop().concat(e.shift()))},result:function(){var n=e;return e=[],t=null,n}}}function Ol(t,e){return Ic(t[0]-e[0])=0;--a)i.point((l=u[a])[0],l[1]);else r(f.x,f.p.x,-1,i);f=f.p}u=(f=f.o).z,d=!d}while(!f.v);i.lineEnd()}}}function Il(t){if(e=t.length){for(var e,n,r=0,i=t[0];++r=0?1:-1,E=T*k,C=E>Mc,S=y*x;if(Rl.add(Fc(S*T*qc(E),g*w+S*Pc(E))),o+=C?k+T*Oc:k,C^d>=n^b>=n){var A=bu(mu(f),mu(v));wu(A);var M=bu(a,A);wu(M);var N=(C^k>=0?-1:1)*Gc(M[2]);(r>N||r===N&&(A[0]||A[1]))&&(s+=C^k>=0?1:-1)}}return(o<-1e-6||o0){for(h||(i.polygonStart(),h=!0),i.lineStart(),t=0;t1&&2&c&&f.push(f.pop().concat(f.shift())),o.push(f.filter(Yl))}return f}}function Yl(t){return t.length>1}function zl(t,e){return((t=t.x)[0]<0?t[1]-Nc-Sc:Nc-t[1])-((e=e.x)[0]<0?e[1]-Nc-Sc:Nc-e[1])}const Ul=jl((function(){return!0}),(function(t){var e,n=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),e=1},point:function(a,o){var s=a>0?Mc:-Mc,c=Ic(a-n);Ic(c-Mc)0?Nc:-Nc),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(s,r),t.point(a,r),e=0):i!==s&&c>=Mc&&(Ic(n-i)Sc?Rc((qc(e)*(a=Pc(r))*qc(n)-qc(r)*(i=Pc(e))*qc(t))/(i*a*o)):(e+r)/2}(n,r,a,o),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(s,r),e=0),t.point(n=a,r=o),i=s},lineEnd:function(){t.lineEnd(),n=r=NaN},clean:function(){return 2-e}}}),(function(t,e,n,r){var i;if(null==t)i=n*Nc,r.point(-Mc,i),r.point(0,i),r.point(Mc,i),r.point(Mc,0),r.point(Mc,-i),r.point(0,-i),r.point(-Mc,-i),r.point(-Mc,0),r.point(-Mc,i);else if(Ic(t[0]-e[0])>Sc){var a=t[0]0,i=Ic(e)>Sc;function a(t,n){return Pc(t)*Pc(n)>e}function o(t,n,r){var i=[1,0,0],a=bu(mu(t),mu(n)),o=vu(a,a),s=a[0],c=o-s*s;if(!c)return!r&&t;var u=e*o/c,l=-e*s/c,h=bu(i,a),f=xu(i,u);_u(f,xu(a,l));var d=h,p=vu(f,d),y=vu(d,d),g=p*p-y*(vu(f,f)-1);if(!(g<0)){var m=$c(g),v=xu(d,(-p-m)/y);if(_u(v,f),v=gu(v),!r)return v;var b,_=t[0],x=n[0],w=t[1],k=n[1];x<_&&(b=_,_=x,x=b);var T=x-_,E=Ic(T-Mc)0^v[1]<(Ic(v[0]-_)Mc^(_<=v[0]&&v[0]<=x)){var C=xu(d,(-p+m)/y);return _u(C,f),[v,gu(C)]}}}function s(e,n){var i=r?t:Mc-t,a=0;return e<-i?a|=1:e>i&&(a|=2),n<-i?a|=4:n>i&&(a|=8),a}return jl(a,(function(t){var e,n,c,u,l;return{lineStart:function(){u=c=!1,l=1},point:function(h,f){var d,p=[h,f],y=a(h,f),g=r?y?0:s(h,f):y?s(h+(h<0?Mc:-Mc),f):0;if(!e&&(u=c=y)&&t.lineStart(),y!==c&&(!(d=o(e,p))||Ol(e,d)||Ol(p,d))&&(p[2]=1),y!==c)l=0,y?(t.lineStart(),d=o(p,e),t.point(d[0],d[1])):(d=o(e,p),t.point(d[0],d[1],2),t.lineEnd()),e=d;else if(i&&e&&r^y){var m;g&n||!(m=o(p,e,!0))||(l=0,r?(t.lineStart(),t.point(m[0][0],m[0][1]),t.point(m[1][0],m[1][1]),t.lineEnd()):(t.point(m[1][0],m[1][1]),t.lineEnd(),t.lineStart(),t.point(m[0][0],m[0][1],3)))}!y||e&&Ol(e,p)||t.point(p[0],p[1]),e=p,c=y,n=g},lineEnd:function(){c&&t.lineEnd(),e=null},clean:function(){return l|(u&&c)<<1}}}),(function(e,r,i,a){Al(a,t,n,i,e,r)}),r?[0,-t]:[-Mc,t-Mc])}var Hl=1e9,$l=-Hl;function Wl(t,e,n,r){function i(i,a){return t<=i&&i<=n&&e<=a&&a<=r}function a(i,a,s,u){var l=0,h=0;if(null==i||(l=o(i,s))!==(h=o(a,s))||c(i,a)<0^s>0)do{u.point(0===l||3===l?t:n,l>1?r:e)}while((l=(l+s+4)%4)!==h);else u.point(a[0],a[1])}function o(r,i){return Ic(r[0]-t)0?0:3:Ic(r[0]-n)0?2:1:Ic(r[1]-e)0?1:0:i>0?3:2}function s(t,e){return c(t.x,e.x)}function c(t,e){var n=o(t,1),r=o(e,1);return n!==r?n-r:0===n?e[1]-t[1]:1===n?t[0]-e[0]:2===n?t[1]-e[1]:e[0]-t[0]}return function(o){var c,u,l,h,f,d,p,y,g,m,v,b=o,_=Dl(),x={point:w,lineStart:function(){x.point=k,u&&u.push(l=[]),m=!0,g=!1,p=y=NaN},lineEnd:function(){c&&(k(h,f),d&&g&&_.rejoin(),c.push(_.result())),x.point=w,g&&b.lineEnd()},polygonStart:function(){b=_,c=[],u=[],v=!0},polygonEnd:function(){var e=function(){for(var e=0,n=0,i=u.length;nr&&(f-a)*(r-o)>(d-o)*(t-a)&&++e:d<=r&&(f-a)*(r-o)<(d-o)*(t-a)&&--e;return e}(),n=v&&e,i=(c=P(c)).length;(n||i)&&(o.polygonStart(),n&&(o.lineStart(),a(null,null,1,o),o.lineEnd()),i&&Ll(c,s,e,a,o),o.polygonEnd()),b=o,c=u=l=null}};function w(t,e){i(t,e)&&b.point(t,e)}function k(a,o){var s=i(a,o);if(u&&l.push([a,o]),m)h=a,f=o,d=s,m=!1,s&&(b.lineStart(),b.point(a,o));else if(s&&g)b.point(a,o);else{var c=[p=Math.max($l,Math.min(Hl,p)),y=Math.max($l,Math.min(Hl,y))],_=[a=Math.max($l,Math.min(Hl,a)),o=Math.max($l,Math.min(Hl,o))];!function(t,e,n,r,i,a){var o,s=t[0],c=t[1],u=0,l=1,h=e[0]-s,f=e[1]-c;if(o=n-s,h||!(o>0)){if(o/=h,h<0){if(o0){if(o>l)return;o>u&&(u=o)}if(o=i-s,h||!(o<0)){if(o/=h,h<0){if(o>l)return;o>u&&(u=o)}else if(h>0){if(o0)){if(o/=f,f<0){if(o0){if(o>l)return;o>u&&(u=o)}if(o=a-c,f||!(o<0)){if(o/=f,f<0){if(o>l)return;o>u&&(u=o)}else if(f>0){if(o0&&(t[0]=s+u*h,t[1]=c+u*f),l<1&&(e[0]=s+l*h,e[1]=c+l*f),!0}}}}}(c,_,t,e,n,r)?s&&(b.lineStart(),b.point(a,o),v=!1):(g||(b.lineStart(),b.point(c[0],c[1])),b.point(_[0],_[1]),s||b.lineEnd(),v=!1)}p=a,y=o,g=s}return x}}function Vl(){var t,e,n,r=0,i=0,a=960,o=500;return n={stream:function(n){return t&&e===n?t:t=Wl(r,i,a,o)(e=n)},extent:function(s){return arguments.length?(r=+s[0][0],i=+s[0][1],a=+s[1][0],o=+s[1][1],t=e=null,n):[[r,i],[a,o]]}}}var Gl,Xl,Zl,Ql=kc(),Kl={sphere:Zc,point:Zc,lineStart:function(){Kl.point=th,Kl.lineEnd=Jl},lineEnd:Zc,polygonStart:Zc,polygonEnd:Zc};function Jl(){Kl.point=Kl.lineEnd=Zc}function th(t,e){Gl=t*=Lc,Xl=qc(e*=Lc),Zl=Pc(e),Kl.point=eh}function eh(t,e){t*=Lc;var n=qc(e*=Lc),r=Pc(e),i=Ic(t-Gl),a=Pc(i),o=r*qc(i),s=Zl*n-Xl*r*a,c=Xl*n+Zl*r*a;Ql.add(Fc($c(o*o+s*s),c)),Gl=t,Xl=n,Zl=r}function nh(t){return Ql.reset(),nu(t,Kl),+Ql}var rh=[null,null],ih={type:"LineString",coordinates:rh};function ah(t,e){return rh[0]=t,rh[1]=e,nh(ih)}var oh={Feature:function(t,e){return ch(t.geometry,e)},FeatureCollection:function(t,e){for(var n=t.features,r=-1,i=n.length;++r0&&(i=ah(t[a],t[a-1]))>0&&n<=i&&r<=i&&(n+r-i)*(1-Math.pow((n-r)/i,2))Sc})).map(c)).concat(k(jc(a/d)*d,i,d).filter((function(t){return Ic(t%y)>Sc})).map(u))}return m.lines=function(){return v().map((function(t){return{type:"LineString",coordinates:t}}))},m.outline=function(){return{type:"Polygon",coordinates:[l(r).concat(h(o).slice(1),l(n).reverse().slice(1),h(s).reverse().slice(1))]}},m.extent=function(t){return arguments.length?m.extentMajor(t).extentMinor(t):m.extentMinor()},m.extentMajor=function(t){return arguments.length?(r=+t[0][0],n=+t[1][0],s=+t[0][1],o=+t[1][1],r>n&&(t=r,r=n,n=t),s>o&&(t=s,s=o,o=t),m.precision(g)):[[r,s],[n,o]]},m.extentMinor=function(n){return arguments.length?(e=+n[0][0],t=+n[1][0],a=+n[0][1],i=+n[1][1],e>t&&(n=e,e=t,t=n),a>i&&(n=a,a=i,i=n),m.precision(g)):[[e,a],[t,i]]},m.step=function(t){return arguments.length?m.stepMajor(t).stepMinor(t):m.stepMinor()},m.stepMajor=function(t){return arguments.length?(p=+t[0],y=+t[1],m):[p,y]},m.stepMinor=function(t){return arguments.length?(f=+t[0],d=+t[1],m):[f,d]},m.precision=function(f){return arguments.length?(g=+f,c=yh(a,i,90),u=gh(e,t,g),l=yh(s,o,90),h=gh(r,n,g),m):g},m.extentMajor([[-180,-89.999999],[180,89.999999]]).extentMinor([[-180,-80.000001],[180,80.000001]])}function vh(){return mh()()}function bh(t,e){var n=t[0]*Lc,r=t[1]*Lc,i=e[0]*Lc,a=e[1]*Lc,o=Pc(r),s=qc(r),c=Pc(a),u=qc(a),l=o*Pc(n),h=o*qc(n),f=c*Pc(i),d=c*qc(i),p=2*Gc($c(Xc(a-r)+o*c*Xc(i-n))),y=qc(p),g=p?function(t){var e=qc(t*=p)/y,n=qc(p-t)/y,r=n*l+e*f,i=n*h+e*d,a=n*s+e*u;return[Fc(i,r)*Bc,Fc(a,$c(r*r+i*i))*Bc]}:function(){return[n*Bc,r*Bc]};return g.distance=p,g}function _h(t){return t}var xh,wh,kh,Th,Eh=kc(),Ch=kc(),Sh={point:Zc,lineStart:Zc,lineEnd:Zc,polygonStart:function(){Sh.lineStart=Ah,Sh.lineEnd=Dh},polygonEnd:function(){Sh.lineStart=Sh.lineEnd=Sh.point=Zc,Eh.add(Ic(Ch)),Ch.reset()},result:function(){var t=Eh/2;return Eh.reset(),t}};function Ah(){Sh.point=Mh}function Mh(t,e){Sh.point=Nh,xh=kh=t,wh=Th=e}function Nh(t,e){Ch.add(Th*t-kh*e),kh=t,Th=e}function Dh(){Nh(xh,wh)}const Oh=Sh;var Bh=1/0,Lh=Bh,Ih=-Bh,Rh=Ih,Fh={point:function(t,e){tIh&&(Ih=t),eRh&&(Rh=e)},lineStart:Zc,lineEnd:Zc,polygonStart:Zc,polygonEnd:Zc,result:function(){var t=[[Bh,Lh],[Ih,Rh]];return Ih=Rh=-(Lh=Bh=1/0),t}};const Ph=Fh;var jh,Yh,zh,Uh,qh=0,Hh=0,$h=0,Wh=0,Vh=0,Gh=0,Xh=0,Zh=0,Qh=0,Kh={point:Jh,lineStart:tf,lineEnd:rf,polygonStart:function(){Kh.lineStart=af,Kh.lineEnd=of},polygonEnd:function(){Kh.point=Jh,Kh.lineStart=tf,Kh.lineEnd=rf},result:function(){var t=Qh?[Xh/Qh,Zh/Qh]:Gh?[Wh/Gh,Vh/Gh]:$h?[qh/$h,Hh/$h]:[NaN,NaN];return qh=Hh=$h=Wh=Vh=Gh=Xh=Zh=Qh=0,t}};function Jh(t,e){qh+=t,Hh+=e,++$h}function tf(){Kh.point=ef}function ef(t,e){Kh.point=nf,Jh(zh=t,Uh=e)}function nf(t,e){var n=t-zh,r=e-Uh,i=$c(n*n+r*r);Wh+=i*(zh+t)/2,Vh+=i*(Uh+e)/2,Gh+=i,Jh(zh=t,Uh=e)}function rf(){Kh.point=Jh}function af(){Kh.point=sf}function of(){cf(jh,Yh)}function sf(t,e){Kh.point=cf,Jh(jh=zh=t,Yh=Uh=e)}function cf(t,e){var n=t-zh,r=e-Uh,i=$c(n*n+r*r);Wh+=i*(zh+t)/2,Vh+=i*(Uh+e)/2,Gh+=i,Xh+=(i=Uh*t-zh*e)*(zh+t),Zh+=i*(Uh+e),Qh+=3*i,Jh(zh=t,Uh=e)}const uf=Kh;function lf(t){this._context=t}lf.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,e){switch(this._point){case 0:this._context.moveTo(t,e),this._point=1;break;case 1:this._context.lineTo(t,e);break;default:this._context.moveTo(t+this._radius,e),this._context.arc(t,e,this._radius,0,Oc)}},result:Zc};var hf,ff,df,pf,yf,gf=kc(),mf={point:Zc,lineStart:function(){mf.point=vf},lineEnd:function(){hf&&bf(ff,df),mf.point=Zc},polygonStart:function(){hf=!0},polygonEnd:function(){hf=null},result:function(){var t=+gf;return gf.reset(),t}};function vf(t,e){mf.point=bf,ff=pf=t,df=yf=e}function bf(t,e){pf-=t,yf-=e,gf.add($c(pf*pf+yf*yf)),pf=t,yf=e}const _f=mf;function xf(){this._string=[]}function wf(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function kf(t,e){var n,r,i=4.5;function a(t){return t&&("function"==typeof i&&r.pointRadius(+i.apply(this,arguments)),nu(t,n(r))),r.result()}return a.area=function(t){return nu(t,n(Oh)),Oh.result()},a.measure=function(t){return nu(t,n(_f)),_f.result()},a.bounds=function(t){return nu(t,n(Ph)),Ph.result()},a.centroid=function(t){return nu(t,n(uf)),uf.result()},a.projection=function(e){return arguments.length?(n=null==e?(t=null,_h):(t=e).stream,a):t},a.context=function(t){return arguments.length?(r=null==t?(e=null,new xf):new lf(e=t),"function"!=typeof i&&r.pointRadius(i),a):e},a.pointRadius=function(t){return arguments.length?(i="function"==typeof t?t:(r.pointRadius(+t),+t),a):i},a.projection(t).context(e)}function Tf(t){return{stream:Ef(t)}}function Ef(t){return function(e){var n=new Cf;for(var r in t)n[r]=t[r];return n.stream=e,n}}function Cf(){}function Sf(t,e,n){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),nu(n,t.stream(Ph)),e(Ph.result()),null!=r&&t.clipExtent(r),t}function Af(t,e,n){return Sf(t,(function(n){var r=e[1][0]-e[0][0],i=e[1][1]-e[0][1],a=Math.min(r/(n[1][0]-n[0][0]),i/(n[1][1]-n[0][1])),o=+e[0][0]+(r-a*(n[1][0]+n[0][0]))/2,s=+e[0][1]+(i-a*(n[1][1]+n[0][1]))/2;t.scale(150*a).translate([o,s])}),n)}function Mf(t,e,n){return Af(t,[[0,0],e],n)}function Nf(t,e,n){return Sf(t,(function(n){var r=+e,i=r/(n[1][0]-n[0][0]),a=(r-i*(n[1][0]+n[0][0]))/2,o=-i*n[0][1];t.scale(150*i).translate([a,o])}),n)}function Df(t,e,n){return Sf(t,(function(n){var r=+e,i=r/(n[1][1]-n[0][1]),a=-i*n[0][0],o=(r-i*(n[1][1]+n[0][1]))/2;t.scale(150*i).translate([a,o])}),n)}xf.prototype={_radius:4.5,_circle:wf(4.5),pointRadius:function(t){return(t=+t)!==this._radius&&(this._radius=t,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,e){switch(this._point){case 0:this._string.push("M",t,",",e),this._point=1;break;case 1:this._string.push("L",t,",",e);break;default:null==this._circle&&(this._circle=wf(this._radius)),this._string.push("M",t,",",e,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}return null}},Cf.prototype={constructor:Cf,point:function(t,e){this.stream.point(t,e)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Of=Pc(30*Lc);function Bf(t,e){return+e?function(t,e){function n(r,i,a,o,s,c,u,l,h,f,d,p,y,g){var m=u-r,v=l-i,b=m*m+v*v;if(b>4*e&&y--){var _=o+f,x=s+d,w=c+p,k=$c(_*_+x*x+w*w),T=Gc(w/=k),E=Ic(Ic(w)-1)e||Ic((m*M+v*N)/b-.5)>.3||o*f+s*d+c*p2?t[2]%360*Lc:0,M()):[g*Bc,m*Bc,v*Bc]},S.angle=function(t){return arguments.length?(b=t%360*Lc,M()):b*Bc},S.reflectX=function(t){return arguments.length?(_=t?-1:1,M()):_<0},S.reflectY=function(t){return arguments.length?(x=t?-1:1,M()):x<0},S.precision=function(t){return arguments.length?(o=Bf(s,C=t*t),N()):$c(C)},S.fitExtent=function(t,e){return Af(S,t,e)},S.fitSize=function(t,e){return Mf(S,t,e)},S.fitWidth=function(t,e){return Nf(S,t,e)},S.fitHeight=function(t,e){return Df(S,t,e)},function(){return e=t.apply(this,arguments),S.invert=e.invert&&A,M()}}function jf(t){var e=0,n=Mc/3,r=Pf(t),i=r(e,n);return i.parallels=function(t){return arguments.length?r(e=t[0]*Lc,n=t[1]*Lc):[e*Bc,n*Bc]},i}function Yf(t,e){var n=qc(t),r=(n+qc(e))/2;if(Ic(r)=.12&&i<.234&&r>=-.425&&r<-.214?s:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:o).invert(t)},l.stream=function(n){return t&&e===n?t:(r=[o.stream(e=n),s.stream(n),c.stream(n)],i=r.length,t={point:function(t,e){for(var n=-1;++n0?e<-Nc+Sc&&(e=-Nc+Sc):e>Nc-Sc&&(e=Nc-Sc);var n=i/Uc(Jf(e),r);return[n*qc(r*t),i-n*Pc(r*t)]}return a.invert=function(t,e){var n=i-e,a=Hc(r)*$c(t*t+n*n),o=Fc(t,Ic(n))*Hc(n);return n*r<0&&(o-=Mc*Hc(t)*Hc(n)),[o/r,2*Rc(Uc(i/a,1/r))-Nc]},a}function ed(){return jf(td).scale(109.5).parallels([30,30])}function nd(t,e){return[t,e]}function rd(){return Ff(nd).scale(152.63)}function id(t,e){var n=Pc(t),r=t===e?qc(t):(n-Pc(e))/(e-t),i=n/r+t;if(Ic(r)2?t[2]+90:90]):[(t=n())[0],t[1],t[2]-90]},n([0,0,90]).scale(159.155)}function Td(t,e){return t.parent===e.parent?1:2}function Ed(t,e){return t+e.x}function Cd(t,e){return Math.max(t,e.y)}function Sd(){var t=Td,e=1,n=1,r=!1;function i(i){var a,o=0;i.eachAfter((function(e){var n=e.children;n?(e.x=function(t){return t.reduce(Ed,0)/t.length}(n),e.y=function(t){return 1+t.reduce(Cd,0)}(n)):(e.x=a?o+=t(e,a):0,e.y=0,a=e)}));var s=function(t){for(var e;e=t.children;)t=e[0];return t}(i),c=function(t){for(var e;e=t.children;)t=e[e.length-1];return t}(i),u=s.x-t(s,c)/2,l=c.x+t(c,s)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*e,t.y=(i.y-t.y)*n}:function(t){t.x=(t.x-u)/(l-u)*e,t.y=(1-(i.y?t.y/i.y:1))*n})}return i.separation=function(e){return arguments.length?(t=e,i):t},i.size=function(t){return arguments.length?(r=!1,e=+t[0],n=+t[1],i):r?null:[e,n]},i.nodeSize=function(t){return arguments.length?(r=!0,e=+t[0],n=+t[1],i):r?[e,n]:null},i}function Ad(t){var e=0,n=t.children,r=n&&n.length;if(r)for(;--r>=0;)e+=n[r].value;else e=1;t.value=e}function Md(t,e){var n,r,i,a,o,s=new Bd(t),c=+t.value&&(s.value=t.value),u=[s];for(null==e&&(e=Nd);n=u.pop();)if(c&&(n.value=+n.data.value),(i=e(n.data))&&(o=i.length))for(n.children=new Array(o),a=o-1;a>=0;--a)u.push(r=n.children[a]=new Bd(i[a])),r.parent=n,r.depth=n.depth+1;return s.eachBefore(Od)}function Nd(t){return t.children}function Dd(t){t.data=t.data.data}function Od(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function Bd(t){this.data=t,this.depth=this.height=0,this.parent=null}hd.invert=function(t,e){for(var n,r=e,i=r*r,a=i*i*i,o=0;o<12&&(a=(i=(r-=n=(r*(od+sd*i+a*(cd+ud*i))-e)/(od+3*sd*i+a*(7*cd+9*ud*i)))*r)*i*i,!(Ic(n)Sc&&--i>0);return[t/(.8707+(a=r*r)*(a*(a*a*a*(.003971-.001529*a)-.013791)-.131979)),r]},vd.invert=$f(Gc),_d.invert=$f((function(t){return 2*Rc(t)})),wd.invert=function(t,e){return[-e,2*Rc(Yc(t))-Nc]},Bd.prototype=Md.prototype={constructor:Bd,count:function(){return this.eachAfter(Ad)},each:function(t){var e,n,r,i,a=this,o=[a];do{for(e=o.reverse(),o=[];a=e.pop();)if(t(a),n=a.children)for(r=0,i=n.length;r=0;--n)i.push(e[n]);return this},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,r=e.children,i=r&&r.length;--i>=0;)n+=r[i].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;for(t=n.pop(),e=r.pop();t===e;)i=t,t=n.pop(),e=r.pop();return i}(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){var t=[];return this.each((function(e){t.push(e)})),t},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return Md(this).eachBefore(Dd)}};var Ld=Array.prototype.slice;function Id(t){for(var e,n,r=0,i=(t=function(t){for(var e,n,r=t.length;r;)n=Math.random()*r--|0,e=t[r],t[r]=t[n],t[n]=e;return t}(Ld.call(t))).length,a=[];r0&&n*n>r*r+i*i}function jd(t,e){for(var n=0;n(o*=o)?(r=(u+o-i)/(2*u),a=Math.sqrt(Math.max(0,o/u-r*r)),n.x=t.x-r*s-a*c,n.y=t.y-r*c+a*s):(r=(u+i-o)/(2*u),a=Math.sqrt(Math.max(0,i/u-r*r)),n.x=e.x+r*s-a*c,n.y=e.y+r*c+a*s)):(n.x=e.x+n.r,n.y=e.y)}function Hd(t,e){var n=t.r+e.r-1e-6,r=e.x-t.x,i=e.y-t.y;return n>0&&n*n>r*r+i*i}function $d(t){var e=t._,n=t.next._,r=e.r+n.r,i=(e.x*n.r+n.x*e.r)/r,a=(e.y*n.r+n.y*e.r)/r;return i*i+a*a}function Wd(t){this._=t,this.next=null,this.previous=null}function Vd(t){if(!(i=t.length))return 0;var e,n,r,i,a,o,s,c,u,l,h;if((e=t[0]).x=0,e.y=0,!(i>1))return e.r;if(n=t[1],e.x=-n.r,n.x=e.r,n.y=0,!(i>2))return e.r+n.r;qd(n,e,r=t[2]),e=new Wd(e),n=new Wd(n),r=new Wd(r),e.next=r.previous=n,n.next=e.previous=r,r.next=n.previous=e;t:for(s=3;s0)throw new Error("cycle");return a}return n.id=function(e){return arguments.length?(t=Zd(e),n):t},n.parentId=function(t){return arguments.length?(e=Zd(t),n):e},n}function fp(t,e){return t.parent===e.parent?1:2}function dp(t){var e=t.children;return e?e[0]:t.t}function pp(t){var e=t.children;return e?e[e.length-1]:t.t}function yp(t,e,n){var r=n/(e.i-t.i);e.c-=r,e.s+=n,t.c+=r,e.z+=n,e.m+=n}function gp(t,e,n){return t.a.parent===e.parent?t.a:n}function mp(t,e){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=e}function vp(){var t=fp,e=1,n=1,r=null;function i(i){var c=function(t){for(var e,n,r,i,a,o=new mp(t,0),s=[o];e=s.pop();)if(r=e._.children)for(e.children=new Array(a=r.length),i=a-1;i>=0;--i)s.push(n=e.children[i]=new mp(r[i],i)),n.parent=e;return(o.parent=new mp(null,0)).children=[o],o}(i);if(c.eachAfter(a),c.parent.m=-c.z,c.eachBefore(o),r)i.eachBefore(s);else{var u=i,l=i,h=i;i.eachBefore((function(t){t.xl.x&&(l=t),t.depth>h.depth&&(h=t)}));var f=u===l?1:t(u,l)/2,d=f-u.x,p=e/(l.x+f+d),y=n/(h.depth||1);i.eachBefore((function(t){t.x=(t.x+d)*p,t.y=t.depth*y}))}return i}function a(e){var n=e.children,r=e.parent.children,i=e.i?r[e.i-1]:null;if(n){!function(t){for(var e,n=0,r=0,i=t.children,a=i.length;--a>=0;)(e=i[a]).z+=n,e.m+=n,n+=e.s+(r+=e.c)}(e);var a=(n[0].z+n[n.length-1].z)/2;i?(e.z=i.z+t(e._,i._),e.m=e.z-a):e.z=a}else i&&(e.z=i.z+t(e._,i._));e.parent.A=function(e,n,r){if(n){for(var i,a=e,o=e,s=n,c=a.parent.children[0],u=a.m,l=o.m,h=s.m,f=c.m;s=pp(s),a=dp(a),s&&a;)c=dp(c),(o=pp(o)).a=e,(i=s.z+h-a.z-u+t(s._,a._))>0&&(yp(gp(s,e,r),e,i),u+=i,l+=i),h+=s.m,u+=a.m,f+=c.m,l+=o.m;s&&!pp(o)&&(o.t=s,o.m+=h-l),a&&!dp(c)&&(c.t=a,c.m+=u-f,r=e)}return r}(e,i,e.parent.A||r[0])}function o(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function s(t){t.x*=e,t.y=t.depth*n}return i.separation=function(e){return arguments.length?(t=e,i):t},i.size=function(t){return arguments.length?(r=!1,e=+t[0],n=+t[1],i):r?null:[e,n]},i.nodeSize=function(t){return arguments.length?(r=!0,e=+t[0],n=+t[1],i):r?[e,n]:null},i}function bp(t,e,n,r,i){for(var a,o=t.children,s=-1,c=o.length,u=t.value&&(i-n)/t.value;++sf&&(f=s),g=l*l*y,(d=Math.max(f/g,g/h))>p){l-=s;break}p=d}m.push(o={value:l,dice:c1?e:1)},n}(_p);function kp(){var t=wp,e=!1,n=1,r=1,i=[0],a=Qd,o=Qd,s=Qd,c=Qd,u=Qd;function l(t){return t.x0=t.y0=0,t.x1=n,t.y1=r,t.eachBefore(h),i=[0],e&&t.eachBefore(ip),t}function h(e){var n=i[e.depth],r=e.x0+n,l=e.y0+n,h=e.x1-n,f=e.y1-n;h=n-1){var l=s[e];return l.x0=i,l.y0=a,l.x1=o,void(l.y1=c)}for(var h=u[e],f=r/2+h,d=e+1,p=n-1;d>>1;u[y]c-a){var v=(i*m+o*g)/r;t(e,d,g,i,a,v,c),t(d,n,m,v,a,o,c)}else{var b=(a*m+c*g)/r;t(e,d,g,i,a,o,b),t(d,n,m,i,b,o,c)}}(0,c,t.value,e,n,r,i)}function Ep(t,e,n,r,i){(1&t.depth?bp:ap)(t,e,n,r,i)}const Cp=function t(e){function n(t,n,r,i,a){if((o=t._squarify)&&o.ratio===e)for(var o,s,c,u,l,h=-1,f=o.length,d=t.value;++h1?e:1)},n}(_p);function Sp(t){var e=t.length;return function(n){return t[Math.max(0,Math.min(e-1,Math.floor(n*e)))]}}function Ap(t,e){var n=dn(+t,+e);return function(t){var e=n(t);return e-360*Math.floor(e/360)}}function Mp(t,e){return t=+t,e=+e,function(n){return Math.round(t*(1-n)+e*n)}}var Np=Math.SQRT2;function Dp(t){return((t=Math.exp(t))+1/t)/2}function Op(t,e){var n,r,i=t[0],a=t[1],o=t[2],s=e[0],c=e[1],u=e[2],l=s-i,h=c-a,f=l*l+h*h;if(f<1e-12)r=Math.log(u/o)/Np,n=function(t){return[i+t*l,a+t*h,o*Math.exp(Np*t*r)]};else{var d=Math.sqrt(f),p=(u*u-o*o+4*f)/(2*o*2*d),y=(u*u-o*o-4*f)/(2*u*2*d),g=Math.log(Math.sqrt(p*p+1)-p),m=Math.log(Math.sqrt(y*y+1)-y);r=(m-g)/Np,n=function(t){var e,n=t*r,s=Dp(g),c=o/(2*d)*(s*(e=Np*n+g,((e=Math.exp(2*e))-1)/(e+1))-function(t){return((t=Math.exp(t))-1/t)/2}(g));return[i+c*l,a+c*h,o*s/Dp(Np*n+g)]}}return n.duration=1e3*r,n}function Bp(t){return function(e,n){var r=t((e=an(e)).h,(n=an(n)).h),i=pn(e.s,n.s),a=pn(e.l,n.l),o=pn(e.opacity,n.opacity);return function(t){return e.h=r(t),e.s=i(t),e.l=a(t),e.opacity=o(t),e+""}}}const Lp=Bp(dn);var Ip=Bp(pn);function Rp(t,e){var n=pn((t=Ta(t)).l,(e=Ta(e)).l),r=pn(t.a,e.a),i=pn(t.b,e.b),a=pn(t.opacity,e.opacity);return function(e){return t.l=n(e),t.a=r(e),t.b=i(e),t.opacity=a(e),t+""}}function Fp(t){return function(e,n){var r=t((e=Oa(e)).h,(n=Oa(n)).h),i=pn(e.c,n.c),a=pn(e.l,n.l),o=pn(e.opacity,n.opacity);return function(t){return e.h=r(t),e.c=i(t),e.l=a(t),e.opacity=o(t),e+""}}}const Pp=Fp(dn);var jp=Fp(pn);function Yp(t){return function e(n){function r(e,r){var i=t((e=Ha(e)).h,(r=Ha(r)).h),a=pn(e.s,r.s),o=pn(e.l,r.l),s=pn(e.opacity,r.opacity);return function(t){return e.h=i(t),e.s=a(t),e.l=o(Math.pow(t,n)),e.opacity=s(t),e+""}}return n=+n,r.gamma=e,r}(1)}const zp=Yp(dn);var Up=Yp(pn);function qp(t,e){for(var n=0,r=e.length-1,i=e[0],a=new Array(r<0?0:r);n1&&Vp(t[n[r-2]],t[n[r-1]],t[i])<=0;)--r;n[r++]=i}return n.slice(0,r)}function Zp(t){if((n=t.length)<3)return null;var e,n,r=new Array(n),i=new Array(n);for(e=0;e=0;--e)u.push(t[r[a[e]][2]]);for(e=+s;es!=u>s&&o<(c-n)*(s-r)/(u-r)+n&&(l=!l),c=n,u=r;return l}function Kp(t){for(var e,n,r=-1,i=t.length,a=t[i-1],o=a[0],s=a[1],c=0;++r1);return t+n*a*Math.sqrt(-2*Math.log(i)/i)}}return n.source=t,n}(Jp),ny=function t(e){function n(){var t=ey.source(e).apply(this,arguments);return function(){return Math.exp(t())}}return n.source=t,n}(Jp),ry=function t(e){function n(t){return function(){for(var n=0,r=0;rr&&(e=n,n=r,r=e),function(t){return Math.max(n,Math.min(r,t))}}function xy(t,e,n){var r=t[0],i=t[1],a=e[0],o=e[1];return i2?wy:xy,i=a=null,h}function h(e){return isNaN(e=+e)?n:(i||(i=r(o.map(t),s,c)))(t(u(e)))}return h.invert=function(n){return u(e((a||(a=r(s,o.map(t),Tn)))(n)))},h.domain=function(t){return arguments.length?(o=uy.call(t,gy),u===vy||(u=_y(o)),l()):o.slice()},h.range=function(t){return arguments.length?(s=ly.call(t),l()):s.slice()},h.rangeRound=function(t){return s=ly.call(t),c=Mp,l()},h.clamp=function(t){return arguments.length?(u=t?_y(o):vy,h):u!==vy},h.interpolate=function(t){return arguments.length?(c=t,l()):c},h.unknown=function(t){return arguments.length?(n=t,h):n},function(n,r){return t=n,e=r,l()}}function Ey(t,e){return Ty()(t,e)}function Cy(t,e,n,r){var i,a=M(t,e,n);switch((r=cc(null==r?",f":r)).type){case"s":var o=Math.max(Math.abs(t),Math.abs(e));return null!=r.precision||isNaN(i=xc(a,o))||(r.precision=i),yc(r,o);case"":case"e":case"g":case"p":case"r":null!=r.precision||isNaN(i=wc(a,Math.max(Math.abs(t),Math.abs(e))))||(r.precision=i-("e"===r.type));break;case"f":case"%":null!=r.precision||isNaN(i=_c(a))||(r.precision=i-2*("%"===r.type))}return pc(r)}function Sy(t){var e=t.domain;return t.ticks=function(t){var n=e();return S(n[0],n[n.length-1],null==t?10:t)},t.tickFormat=function(t,n){var r=e();return Cy(r[0],r[r.length-1],null==t?10:t,n)},t.nice=function(n){null==n&&(n=10);var r,i=e(),a=0,o=i.length-1,s=i[a],c=i[o];return c0?r=A(s=Math.floor(s/r)*r,c=Math.ceil(c/r)*r,n):r<0&&(r=A(s=Math.ceil(s*r)/r,c=Math.floor(c*r)/r,n)),r>0?(i[a]=Math.floor(s/r)*r,i[o]=Math.ceil(c/r)*r,e(i)):r<0&&(i[a]=Math.ceil(s*r)/r,i[o]=Math.floor(c*r)/r,e(i)),t},t}function Ay(){var t=Ey(vy,vy);return t.copy=function(){return ky(t,Ay())},oy.apply(t,arguments),Sy(t)}function My(t){var e;function n(t){return isNaN(t=+t)?e:t}return n.invert=n,n.domain=n.range=function(e){return arguments.length?(t=uy.call(e,gy),n):t.slice()},n.unknown=function(t){return arguments.length?(e=t,n):e},n.copy=function(){return My(t).unknown(e)},t=arguments.length?uy.call(t,gy):[0,1],Sy(n)}function Ny(t,e){var n,r=0,i=(t=t.slice()).length-1,a=t[r],o=t[i];return o0){for(;fc)break;y.push(h)}}else for(;f=1;--l)if(!((h=u*l)c)break;y.push(h)}}else y=S(f,d,Math.min(d-f,p)).map(n);return r?y.reverse():y},r.tickFormat=function(t,i){if(null==i&&(i=10===a?".0e":","),"function"!=typeof i&&(i=pc(i)),t===1/0)return i;null==t&&(t=10);var o=Math.max(1,a*t/r.ticks().length);return function(t){var r=t/n(Math.round(e(t)));return r*a0?r[i-1]:e[0],i=r?[i[r-1],n]:[i[o-1],i[o]]},o.unknown=function(e){return arguments.length?(t=e,o):o},o.thresholds=function(){return i.slice()},o.copy=function(){return Zy().domain([e,n]).range(a).unknown(t)},oy.apply(Sy(o),arguments)}function Qy(){var t,e=[.5],n=[0,1],r=1;function i(i){return i<=i?n[u(e,i,0,r)]:t}return i.domain=function(t){return arguments.length?(e=ly.call(t),r=Math.min(e.length,n.length-1),i):e.slice()},i.range=function(t){return arguments.length?(n=ly.call(t),r=Math.min(e.length,n.length-1),i):n.slice()},i.invertExtent=function(t){var r=n.indexOf(t);return[e[r-1],e[r]]},i.unknown=function(e){return arguments.length?(t=e,i):t},i.copy=function(){return Qy().domain(e).range(n).unknown(t)},oy.apply(i,arguments)}var Ky=new Date,Jy=new Date;function tg(t,e,n,r){function i(e){return t(e=0===arguments.length?new Date:new Date(+e)),e}return i.floor=function(e){return t(e=new Date(+e)),e},i.ceil=function(n){return t(n=new Date(n-1)),e(n,1),t(n),n},i.round=function(t){var e=i(t),n=i.ceil(t);return t-e0))return s;do{s.push(o=new Date(+n)),e(n,a),t(n)}while(o=e)for(;t(e),!n(e);)e.setTime(e-1)}),(function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;e(t,-1),!n(t););else for(;--r>=0;)for(;e(t,1),!n(t););}))},n&&(i.count=function(e,r){return Ky.setTime(+e),Jy.setTime(+r),t(Ky),t(Jy),Math.floor(n(Ky,Jy))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(e){return r(e)%t==0}:function(e){return i.count(0,e)%t==0}):i:null}),i}var eg=tg((function(t){t.setMonth(0,1),t.setHours(0,0,0,0)}),(function(t,e){t.setFullYear(t.getFullYear()+e)}),(function(t,e){return e.getFullYear()-t.getFullYear()}),(function(t){return t.getFullYear()}));eg.every=function(t){return isFinite(t=Math.floor(t))&&t>0?tg((function(e){e.setFullYear(Math.floor(e.getFullYear()/t)*t),e.setMonth(0,1),e.setHours(0,0,0,0)}),(function(e,n){e.setFullYear(e.getFullYear()+n*t)})):null};const ng=eg;var rg=eg.range,ig=tg((function(t){t.setDate(1),t.setHours(0,0,0,0)}),(function(t,e){t.setMonth(t.getMonth()+e)}),(function(t,e){return e.getMonth()-t.getMonth()+12*(e.getFullYear()-t.getFullYear())}),(function(t){return t.getMonth()}));const ag=ig;var og=ig.range,sg=1e3,cg=6e4,ug=36e5,lg=864e5,hg=6048e5;function fg(t){return tg((function(e){e.setDate(e.getDate()-(e.getDay()+7-t)%7),e.setHours(0,0,0,0)}),(function(t,e){t.setDate(t.getDate()+7*e)}),(function(t,e){return(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*cg)/hg}))}var dg=fg(0),pg=fg(1),yg=fg(2),gg=fg(3),mg=fg(4),vg=fg(5),bg=fg(6),_g=dg.range,xg=pg.range,wg=yg.range,kg=gg.range,Tg=mg.range,Eg=vg.range,Cg=bg.range,Sg=tg((function(t){t.setHours(0,0,0,0)}),(function(t,e){t.setDate(t.getDate()+e)}),(function(t,e){return(e-t-(e.getTimezoneOffset()-t.getTimezoneOffset())*cg)/lg}),(function(t){return t.getDate()-1}));const Ag=Sg;var Mg=Sg.range,Ng=tg((function(t){t.setTime(t-t.getMilliseconds()-t.getSeconds()*sg-t.getMinutes()*cg)}),(function(t,e){t.setTime(+t+e*ug)}),(function(t,e){return(e-t)/ug}),(function(t){return t.getHours()}));const Dg=Ng;var Og=Ng.range,Bg=tg((function(t){t.setTime(t-t.getMilliseconds()-t.getSeconds()*sg)}),(function(t,e){t.setTime(+t+e*cg)}),(function(t,e){return(e-t)/cg}),(function(t){return t.getMinutes()}));const Lg=Bg;var Ig=Bg.range,Rg=tg((function(t){t.setTime(t-t.getMilliseconds())}),(function(t,e){t.setTime(+t+e*sg)}),(function(t,e){return(e-t)/sg}),(function(t){return t.getUTCSeconds()}));const Fg=Rg;var Pg=Rg.range,jg=tg((function(){}),(function(t,e){t.setTime(+t+e)}),(function(t,e){return e-t}));jg.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?tg((function(e){e.setTime(Math.floor(e/t)*t)}),(function(e,n){e.setTime(+e+n*t)}),(function(e,n){return(n-e)/t})):jg:null};const Yg=jg;var zg=jg.range;function Ug(t){return tg((function(e){e.setUTCDate(e.getUTCDate()-(e.getUTCDay()+7-t)%7),e.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+7*e)}),(function(t,e){return(e-t)/hg}))}var qg=Ug(0),Hg=Ug(1),$g=Ug(2),Wg=Ug(3),Vg=Ug(4),Gg=Ug(5),Xg=Ug(6),Zg=qg.range,Qg=Hg.range,Kg=$g.range,Jg=Wg.range,tm=Vg.range,em=Gg.range,nm=Xg.range,rm=tg((function(t){t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCDate(t.getUTCDate()+e)}),(function(t,e){return(e-t)/lg}),(function(t){return t.getUTCDate()-1}));const im=rm;var am=rm.range,om=tg((function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)}),(function(t,e){t.setUTCFullYear(t.getUTCFullYear()+e)}),(function(t,e){return e.getUTCFullYear()-t.getUTCFullYear()}),(function(t){return t.getUTCFullYear()}));om.every=function(t){return isFinite(t=Math.floor(t))&&t>0?tg((function(e){e.setUTCFullYear(Math.floor(e.getUTCFullYear()/t)*t),e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)}),(function(e,n){e.setUTCFullYear(e.getUTCFullYear()+n*t)})):null};const sm=om;var cm=om.range;function um(t){if(0<=t.y&&t.y<100){var e=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return e.setFullYear(t.y),e}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function lm(t){if(0<=t.y&&t.y<100){var e=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return e.setUTCFullYear(t.y),e}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function hm(t,e,n){return{y:t,m:e,d:n,H:0,M:0,S:0,L:0}}function fm(t){var e=t.dateTime,n=t.date,r=t.time,i=t.periods,a=t.days,o=t.shortDays,s=t.months,c=t.shortMonths,u=Tm(i),l=Em(i),h=Tm(a),f=Em(a),d=Tm(o),p=Em(o),y=Tm(s),g=Em(s),m=Tm(c),v=Em(c),b={a:function(t){return o[t.getDay()]},A:function(t){return a[t.getDay()]},b:function(t){return c[t.getMonth()]},B:function(t){return s[t.getMonth()]},c:null,d:Wm,e:Wm,f:Qm,g:cv,G:lv,H:Vm,I:Gm,j:Xm,L:Zm,m:Km,M:Jm,p:function(t){return i[+(t.getHours()>=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:Bv,s:Lv,S:tv,u:ev,U:nv,V:iv,w:av,W:ov,x:null,X:null,y:sv,Y:uv,Z:hv,"%":Ov},_={a:function(t){return o[t.getUTCDay()]},A:function(t){return a[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return s[t.getUTCMonth()]},c:null,d:fv,e:fv,f:mv,g:Av,G:Nv,H:dv,I:pv,j:yv,L:gv,m:vv,M:bv,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:Bv,s:Lv,S:_v,u:xv,U:wv,V:Tv,w:Ev,W:Cv,x:null,X:null,y:Sv,Y:Mv,Z:Dv,"%":Ov},x={a:function(t,e,n){var r=d.exec(e.slice(n));return r?(t.w=p[r[0].toLowerCase()],n+r[0].length):-1},A:function(t,e,n){var r=h.exec(e.slice(n));return r?(t.w=f[r[0].toLowerCase()],n+r[0].length):-1},b:function(t,e,n){var r=m.exec(e.slice(n));return r?(t.m=v[r[0].toLowerCase()],n+r[0].length):-1},B:function(t,e,n){var r=y.exec(e.slice(n));return r?(t.m=g[r[0].toLowerCase()],n+r[0].length):-1},c:function(t,n,r){return T(t,e,n,r)},d:Rm,e:Rm,f:Um,g:Om,G:Dm,H:Pm,I:Pm,j:Fm,L:zm,m:Im,M:jm,p:function(t,e,n){var r=u.exec(e.slice(n));return r?(t.p=l[r[0].toLowerCase()],n+r[0].length):-1},q:Lm,Q:Hm,s:$m,S:Ym,u:Sm,U:Am,V:Mm,w:Cm,W:Nm,x:function(t,e,r){return T(t,n,e,r)},X:function(t,e,n){return T(t,r,e,n)},y:Om,Y:Dm,Z:Bm,"%":qm};function w(t,e){return function(n){var r,i,a,o=[],s=-1,c=0,u=t.length;for(n instanceof Date||(n=new Date(+n));++s53)return null;"w"in a||(a.w=1),"Z"in a?(i=(r=lm(hm(a.y,0,1))).getUTCDay(),r=i>4||0===i?Hg.ceil(r):Hg(r),r=im.offset(r,7*(a.V-1)),a.y=r.getUTCFullYear(),a.m=r.getUTCMonth(),a.d=r.getUTCDate()+(a.w+6)%7):(i=(r=um(hm(a.y,0,1))).getDay(),r=i>4||0===i?pg.ceil(r):pg(r),r=Ag.offset(r,7*(a.V-1)),a.y=r.getFullYear(),a.m=r.getMonth(),a.d=r.getDate()+(a.w+6)%7)}else("W"in a||"U"in a)&&("w"in a||(a.w="u"in a?a.u%7:"W"in a?1:0),i="Z"in a?lm(hm(a.y,0,1)).getUTCDay():um(hm(a.y,0,1)).getDay(),a.m=0,a.d="W"in a?(a.w+6)%7+7*a.W-(i+5)%7:a.w+7*a.U-(i+6)%7);return"Z"in a?(a.H+=a.Z/100|0,a.M+=a.Z%100,lm(a)):um(a)}}function T(t,e,n,r){for(var i,a,o=0,s=e.length,c=n.length;o=c)return-1;if(37===(i=e.charCodeAt(o++))){if(i=e.charAt(o++),!(a=x[i in vm?e.charAt(o++):i])||(r=a(t,n,r))<0)return-1}else if(i!=n.charCodeAt(r++))return-1}return r}return b.x=w(n,b),b.X=w(r,b),b.c=w(e,b),_.x=w(n,_),_.X=w(r,_),_.c=w(e,_),{format:function(t){var e=w(t+="",b);return e.toString=function(){return t},e},parse:function(t){var e=k(t+="",!1);return e.toString=function(){return t},e},utcFormat:function(t){var e=w(t+="",_);return e.toString=function(){return t},e},utcParse:function(t){var e=k(t+="",!0);return e.toString=function(){return t},e}}}var dm,pm,ym,gm,mm,vm={"-":"",_:" ",0:"0"},bm=/^\s*\d+/,_m=/^%/,xm=/[\\^$*+?|[\]().{}]/g;function wm(t,e,n){var r=t<0?"-":"",i=(r?-t:t)+"",a=i.length;return r+(a68?1900:2e3),n+r[0].length):-1}function Bm(t,e,n){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(e.slice(n,n+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function Lm(t,e,n){var r=bm.exec(e.slice(n,n+1));return r?(t.q=3*r[0]-3,n+r[0].length):-1}function Im(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.m=r[0]-1,n+r[0].length):-1}function Rm(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.d=+r[0],n+r[0].length):-1}function Fm(t,e,n){var r=bm.exec(e.slice(n,n+3));return r?(t.m=0,t.d=+r[0],n+r[0].length):-1}function Pm(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.H=+r[0],n+r[0].length):-1}function jm(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.M=+r[0],n+r[0].length):-1}function Ym(t,e,n){var r=bm.exec(e.slice(n,n+2));return r?(t.S=+r[0],n+r[0].length):-1}function zm(t,e,n){var r=bm.exec(e.slice(n,n+3));return r?(t.L=+r[0],n+r[0].length):-1}function Um(t,e,n){var r=bm.exec(e.slice(n,n+6));return r?(t.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function qm(t,e,n){var r=_m.exec(e.slice(n,n+1));return r?n+r[0].length:-1}function Hm(t,e,n){var r=bm.exec(e.slice(n));return r?(t.Q=+r[0],n+r[0].length):-1}function $m(t,e,n){var r=bm.exec(e.slice(n));return r?(t.s=+r[0],n+r[0].length):-1}function Wm(t,e){return wm(t.getDate(),e,2)}function Vm(t,e){return wm(t.getHours(),e,2)}function Gm(t,e){return wm(t.getHours()%12||12,e,2)}function Xm(t,e){return wm(1+Ag.count(ng(t),t),e,3)}function Zm(t,e){return wm(t.getMilliseconds(),e,3)}function Qm(t,e){return Zm(t,e)+"000"}function Km(t,e){return wm(t.getMonth()+1,e,2)}function Jm(t,e){return wm(t.getMinutes(),e,2)}function tv(t,e){return wm(t.getSeconds(),e,2)}function ev(t){var e=t.getDay();return 0===e?7:e}function nv(t,e){return wm(dg.count(ng(t)-1,t),e,2)}function rv(t){var e=t.getDay();return e>=4||0===e?mg(t):mg.ceil(t)}function iv(t,e){return t=rv(t),wm(mg.count(ng(t),t)+(4===ng(t).getDay()),e,2)}function av(t){return t.getDay()}function ov(t,e){return wm(pg.count(ng(t)-1,t),e,2)}function sv(t,e){return wm(t.getFullYear()%100,e,2)}function cv(t,e){return wm((t=rv(t)).getFullYear()%100,e,2)}function uv(t,e){return wm(t.getFullYear()%1e4,e,4)}function lv(t,e){var n=t.getDay();return wm((t=n>=4||0===n?mg(t):mg.ceil(t)).getFullYear()%1e4,e,4)}function hv(t){var e=t.getTimezoneOffset();return(e>0?"-":(e*=-1,"+"))+wm(e/60|0,"0",2)+wm(e%60,"0",2)}function fv(t,e){return wm(t.getUTCDate(),e,2)}function dv(t,e){return wm(t.getUTCHours(),e,2)}function pv(t,e){return wm(t.getUTCHours()%12||12,e,2)}function yv(t,e){return wm(1+im.count(sm(t),t),e,3)}function gv(t,e){return wm(t.getUTCMilliseconds(),e,3)}function mv(t,e){return gv(t,e)+"000"}function vv(t,e){return wm(t.getUTCMonth()+1,e,2)}function bv(t,e){return wm(t.getUTCMinutes(),e,2)}function _v(t,e){return wm(t.getUTCSeconds(),e,2)}function xv(t){var e=t.getUTCDay();return 0===e?7:e}function wv(t,e){return wm(qg.count(sm(t)-1,t),e,2)}function kv(t){var e=t.getUTCDay();return e>=4||0===e?Vg(t):Vg.ceil(t)}function Tv(t,e){return t=kv(t),wm(Vg.count(sm(t),t)+(4===sm(t).getUTCDay()),e,2)}function Ev(t){return t.getUTCDay()}function Cv(t,e){return wm(Hg.count(sm(t)-1,t),e,2)}function Sv(t,e){return wm(t.getUTCFullYear()%100,e,2)}function Av(t,e){return wm((t=kv(t)).getUTCFullYear()%100,e,2)}function Mv(t,e){return wm(t.getUTCFullYear()%1e4,e,4)}function Nv(t,e){var n=t.getUTCDay();return wm((t=n>=4||0===n?Vg(t):Vg.ceil(t)).getUTCFullYear()%1e4,e,4)}function Dv(){return"+0000"}function Ov(){return"%"}function Bv(t){return+t}function Lv(t){return Math.floor(+t/1e3)}function Iv(t){return dm=fm(t),pm=dm.format,ym=dm.parse,gm=dm.utcFormat,mm=dm.utcParse,dm}Iv({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Rv=31536e6;function Fv(t){return new Date(t)}function Pv(t){return t instanceof Date?+t:+new Date(+t)}function jv(t,e,n,r,i,o,s,c,u){var l=Ey(vy,vy),h=l.invert,f=l.domain,d=u(".%L"),p=u(":%S"),y=u("%I:%M"),g=u("%I %p"),m=u("%a %d"),v=u("%b %d"),b=u("%B"),_=u("%Y"),x=[[s,1,1e3],[s,5,5e3],[s,15,15e3],[s,30,3e4],[o,1,6e4],[o,5,3e5],[o,15,9e5],[o,30,18e5],[i,1,36e5],[i,3,108e5],[i,6,216e5],[i,12,432e5],[r,1,864e5],[r,2,1728e5],[n,1,6048e5],[e,1,2592e6],[e,3,7776e6],[t,1,Rv]];function w(a){return(s(a)1)&&(t-=Math.floor(t));var e=Math.abs(t-.5);return S_.h=360*t-100,S_.s=1.5-1.5*e,S_.l=.8-.9*e,S_+""}var M_=Qe(),N_=Math.PI/3,D_=2*Math.PI/3;function O_(t){var e;return t=(.5-t)*Math.PI,M_.r=255*(e=Math.sin(t))*e,M_.g=255*(e=Math.sin(t+N_))*e,M_.b=255*(e=Math.sin(t+D_))*e,M_+""}function B_(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(34.61+t*(1172.33-t*(10793.56-t*(33300.12-t*(38394.49-14825.05*t)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+t*(557.33+t*(1225.33-t*(3574.96-t*(1073.77+707.56*t)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+t*(3211.1-t*(15327.97-t*(27814-t*(22569.18-6838.66*t)))))))+")"}function L_(t){var e=t.length;return function(n){return t[Math.max(0,Math.min(e-1,Math.floor(n*e)))]}}const I_=L_(hb("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725"));var R_=L_(hb("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),F_=L_(hb("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),P_=L_(hb("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921"));function j_(t){return Te(ie(t).call(document.documentElement))}var Y_=0;function z_(){return new U_}function U_(){this._="@"+(++Y_).toString(36)}function q_(t){return"string"==typeof t?new xe([document.querySelectorAll(t)],[document.documentElement]):new xe([null==t?[]:t],_e)}function H_(t,e){null==e&&(e=Nn().touches);for(var n=0,r=e?e.length:0,i=new Array(r);n1?0:t<-1?tx:Math.acos(t)}function ix(t){return t>=1?ex:t<=-1?-ex:Math.asin(t)}function ax(t){return t.innerRadius}function ox(t){return t.outerRadius}function sx(t){return t.startAngle}function cx(t){return t.endAngle}function ux(t){return t&&t.padAngle}function lx(t,e,n,r,i,a,o,s){var c=n-t,u=r-e,l=o-i,h=s-a,f=h*c-l*u;if(!(f*fN*N+D*D&&(T=C,E=S),{cx:T,cy:E,x01:-l,y01:-h,x11:T*(i/x-1),y11:E*(i/x-1)}}function fx(){var t=ax,e=ox,n=$_(0),r=null,i=sx,a=cx,o=ux,s=null;function c(){var c,u,l=+t.apply(this,arguments),h=+e.apply(this,arguments),f=i.apply(this,arguments)-ex,d=a.apply(this,arguments)-ex,p=W_(d-f),y=d>f;if(s||(s=c=Wi()),hJ_)if(p>nx-J_)s.moveTo(h*G_(f),h*Q_(f)),s.arc(0,0,h,f,d,!y),l>J_&&(s.moveTo(l*G_(d),l*Q_(d)),s.arc(0,0,l,d,f,y));else{var g,m,v=f,b=d,_=f,x=d,w=p,k=p,T=o.apply(this,arguments)/2,E=T>J_&&(r?+r.apply(this,arguments):K_(l*l+h*h)),C=Z_(W_(h-l)/2,+n.apply(this,arguments)),S=C,A=C;if(E>J_){var M=ix(E/l*Q_(T)),N=ix(E/h*Q_(T));(w-=2*M)>J_?(_+=M*=y?1:-1,x-=M):(w=0,_=x=(f+d)/2),(k-=2*N)>J_?(v+=N*=y?1:-1,b-=N):(k=0,v=b=(f+d)/2)}var D=h*G_(v),O=h*Q_(v),B=l*G_(x),L=l*Q_(x);if(C>J_){var I,R=h*G_(b),F=h*Q_(b),P=l*G_(_),j=l*Q_(_);if(pJ_?A>J_?(g=hx(P,j,D,O,h,A,y),m=hx(R,F,B,L,h,A,y),s.moveTo(g.cx+g.x01,g.cy+g.y01),AJ_&&w>J_?S>J_?(g=hx(B,L,R,F,l,-S,y),m=hx(D,O,P,j,l,-S,y),s.lineTo(g.cx+g.x01,g.cy+g.y01),S=l;--h)s.point(g[h],m[h]);s.lineEnd(),s.areaEnd()}y&&(g[u]=+t(f,u,c),m[u]=+n(f,u,c),s.point(e?+e(f,u,c):g[u],r?+r(f,u,c):m[u]))}if(d)return s=null,d+""||null}function u(){return mx().defined(i).curve(o).context(a)}return c.x=function(n){return arguments.length?(t="function"==typeof n?n:$_(+n),e=null,c):t},c.x0=function(e){return arguments.length?(t="function"==typeof e?e:$_(+e),c):t},c.x1=function(t){return arguments.length?(e=null==t?null:"function"==typeof t?t:$_(+t),c):e},c.y=function(t){return arguments.length?(n="function"==typeof t?t:$_(+t),r=null,c):n},c.y0=function(t){return arguments.length?(n="function"==typeof t?t:$_(+t),c):n},c.y1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:$_(+t),c):r},c.lineX0=c.lineY0=function(){return u().x(t).y(n)},c.lineY1=function(){return u().x(t).y(r)},c.lineX1=function(){return u().x(e).y(n)},c.defined=function(t){return arguments.length?(i="function"==typeof t?t:$_(!!t),c):i},c.curve=function(t){return arguments.length?(o=t,null!=a&&(s=o(a)),c):o},c.context=function(t){return arguments.length?(null==t?a=s=null:s=o(a=t),c):a},c}function bx(t,e){return et?1:e>=t?0:NaN}function _x(t){return t}function xx(){var t=_x,e=bx,n=null,r=$_(0),i=$_(nx),a=$_(0);function o(o){var s,c,u,l,h,f=o.length,d=0,p=new Array(f),y=new Array(f),g=+r.apply(this,arguments),m=Math.min(nx,Math.max(-nx,i.apply(this,arguments)-g)),v=Math.min(Math.abs(m)/f,a.apply(this,arguments)),b=v*(m<0?-1:1);for(s=0;s0&&(d+=h);for(null!=e?p.sort((function(t,n){return e(y[t],y[n])})):null!=n&&p.sort((function(t,e){return n(o[t],o[e])})),s=0,u=d?(m-f*b)/d:0;s0?h*u:0)+b,y[c]={data:o[c],index:s,value:h,startAngle:g,endAngle:l,padAngle:v};return y}return o.value=function(e){return arguments.length?(t="function"==typeof e?e:$_(+e),o):t},o.sortValues=function(t){return arguments.length?(e=t,n=null,o):e},o.sort=function(t){return arguments.length?(n=t,e=null,o):n},o.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:$_(+t),o):r},o.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:$_(+t),o):i},o.padAngle=function(t){return arguments.length?(a="function"==typeof t?t:$_(+t),o):a},o}dx.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;default:this._context.lineTo(t,e)}}};var wx=Tx(px);function kx(t){this._curve=t}function Tx(t){function e(e){return new kx(t(e))}return e._curve=t,e}function Ex(t){var e=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?e(Tx(t)):e()._curve},t}function Cx(){return Ex(mx().curve(wx))}function Sx(){var t=vx().curve(wx),e=t.curve,n=t.lineX0,r=t.lineX1,i=t.lineY0,a=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Ex(n())},delete t.lineX0,t.lineEndAngle=function(){return Ex(r())},delete t.lineX1,t.lineInnerRadius=function(){return Ex(i())},delete t.lineY0,t.lineOuterRadius=function(){return Ex(a())},delete t.lineY1,t.curve=function(t){return arguments.length?e(Tx(t)):e()._curve},t}function Ax(t,e){return[(e=+e)*Math.cos(t-=Math.PI/2),e*Math.sin(t)]}kx.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,e){this._curve.point(e*Math.sin(t),e*-Math.cos(t))}};var Mx=Array.prototype.slice;function Nx(t){return t.source}function Dx(t){return t.target}function Ox(t){var e=Nx,n=Dx,r=yx,i=gx,a=null;function o(){var o,s=Mx.call(arguments),c=e.apply(this,s),u=n.apply(this,s);if(a||(a=o=Wi()),t(a,+r.apply(this,(s[0]=c,s)),+i.apply(this,s),+r.apply(this,(s[0]=u,s)),+i.apply(this,s)),o)return a=null,o+""||null}return o.source=function(t){return arguments.length?(e=t,o):e},o.target=function(t){return arguments.length?(n=t,o):n},o.x=function(t){return arguments.length?(r="function"==typeof t?t:$_(+t),o):r},o.y=function(t){return arguments.length?(i="function"==typeof t?t:$_(+t),o):i},o.context=function(t){return arguments.length?(a=null==t?null:t,o):a},o}function Bx(t,e,n,r,i){t.moveTo(e,n),t.bezierCurveTo(e=(e+r)/2,n,e,i,r,i)}function Lx(t,e,n,r,i){t.moveTo(e,n),t.bezierCurveTo(e,n=(n+i)/2,r,n,r,i)}function Ix(t,e,n,r,i){var a=Ax(e,n),o=Ax(e,n=(n+i)/2),s=Ax(r,n),c=Ax(r,i);t.moveTo(a[0],a[1]),t.bezierCurveTo(o[0],o[1],s[0],s[1],c[0],c[1])}function Rx(){return Ox(Bx)}function Fx(){return Ox(Lx)}function Px(){var t=Ox(Ix);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t}const jx={draw:function(t,e){var n=Math.sqrt(e/tx);t.moveTo(n,0),t.arc(0,0,n,0,nx)}},Yx={draw:function(t,e){var n=Math.sqrt(e/5)/2;t.moveTo(-3*n,-n),t.lineTo(-n,-n),t.lineTo(-n,-3*n),t.lineTo(n,-3*n),t.lineTo(n,-n),t.lineTo(3*n,-n),t.lineTo(3*n,n),t.lineTo(n,n),t.lineTo(n,3*n),t.lineTo(-n,3*n),t.lineTo(-n,n),t.lineTo(-3*n,n),t.closePath()}};var zx=Math.sqrt(1/3),Ux=2*zx;const qx={draw:function(t,e){var n=Math.sqrt(e/Ux),r=n*zx;t.moveTo(0,-n),t.lineTo(r,0),t.lineTo(0,n),t.lineTo(-r,0),t.closePath()}};var Hx=Math.sin(tx/10)/Math.sin(7*tx/10),$x=Math.sin(nx/10)*Hx,Wx=-Math.cos(nx/10)*Hx;const Vx={draw:function(t,e){var n=Math.sqrt(.8908130915292852*e),r=$x*n,i=Wx*n;t.moveTo(0,-n),t.lineTo(r,i);for(var a=1;a<5;++a){var o=nx*a/5,s=Math.cos(o),c=Math.sin(o);t.lineTo(c*n,-s*n),t.lineTo(s*r-c*i,c*r+s*i)}t.closePath()}},Gx={draw:function(t,e){var n=Math.sqrt(e),r=-n/2;t.rect(r,r,n,n)}};var Xx=Math.sqrt(3);const Zx={draw:function(t,e){var n=-Math.sqrt(e/(3*Xx));t.moveTo(0,2*n),t.lineTo(-Xx*n,-n),t.lineTo(Xx*n,-n),t.closePath()}};var Qx=-.5,Kx=Math.sqrt(3)/2,Jx=1/Math.sqrt(12),tw=3*(Jx/2+1);const ew={draw:function(t,e){var n=Math.sqrt(e/tw),r=n/2,i=n*Jx,a=r,o=n*Jx+n,s=-a,c=o;t.moveTo(r,i),t.lineTo(a,o),t.lineTo(s,c),t.lineTo(Qx*r-Kx*i,Kx*r+Qx*i),t.lineTo(Qx*a-Kx*o,Kx*a+Qx*o),t.lineTo(Qx*s-Kx*c,Kx*s+Qx*c),t.lineTo(Qx*r+Kx*i,Qx*i-Kx*r),t.lineTo(Qx*a+Kx*o,Qx*o-Kx*a),t.lineTo(Qx*s+Kx*c,Qx*c-Kx*s),t.closePath()}};var nw=[jx,Yx,qx,Gx,Vx,Zx,ew];function rw(){var t=$_(jx),e=$_(64),n=null;function r(){var r;if(n||(n=r=Wi()),t.apply(this,arguments).draw(n,+e.apply(this,arguments)),r)return n=null,r+""||null}return r.type=function(e){return arguments.length?(t="function"==typeof e?e:$_(e),r):t},r.size=function(t){return arguments.length?(e="function"==typeof t?t:$_(+t),r):e},r.context=function(t){return arguments.length?(n=null==t?null:t,r):n},r}function iw(){}function aw(t,e,n){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+e)/6,(t._y0+4*t._y1+n)/6)}function ow(t){this._context=t}function sw(t){return new ow(t)}function cw(t){this._context=t}function uw(t){return new cw(t)}function lw(t){this._context=t}function hw(t){return new lw(t)}function fw(t,e){this._basis=new ow(t),this._beta=e}ow.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:aw(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:aw(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},cw.prototype={areaStart:iw,areaEnd:iw,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._x2=t,this._y2=e;break;case 1:this._point=2,this._x3=t,this._y3=e;break;case 2:this._point=3,this._x4=t,this._y4=e,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+e)/6);break;default:aw(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},lw.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var n=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+e)/6;this._line?this._context.lineTo(n,r):this._context.moveTo(n,r);break;case 3:this._point=4;default:aw(this,t,e)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=e}},fw.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,e=this._y,n=t.length-1;if(n>0)for(var r,i=t[0],a=e[0],o=t[n]-i,s=e[n]-a,c=-1;++c<=n;)r=c/n,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*o),this._beta*e[c]+(1-this._beta)*(a+r*s));this._x=this._y=null,this._basis.lineEnd()},point:function(t,e){this._x.push(+t),this._y.push(+e)}};const dw=function t(e){function n(t){return 1===e?new ow(t):new fw(t,e)}return n.beta=function(e){return t(+e)},n}(.85);function pw(t,e,n){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-e),t._y2+t._k*(t._y1-n),t._x2,t._y2)}function yw(t,e){this._context=t,this._k=(1-e)/6}yw.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:pw(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2,this._x1=t,this._y1=e;break;case 2:this._point=3;default:pw(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const gw=function t(e){function n(t){return new yw(t,e)}return n.tension=function(e){return t(+e)},n}(0);function mw(t,e){this._context=t,this._k=(1-e)/6}mw.prototype={areaStart:iw,areaEnd:iw,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._x3=t,this._y3=e;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=e);break;case 2:this._point=3,this._x5=t,this._y5=e;break;default:pw(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const vw=function t(e){function n(t){return new mw(t,e)}return n.tension=function(e){return t(+e)},n}(0);function bw(t,e){this._context=t,this._k=(1-e)/6}bw.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:pw(this,t,e)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const _w=function t(e){function n(t){return new bw(t,e)}return n.tension=function(e){return t(+e)},n}(0);function xw(t,e,n){var r=t._x1,i=t._y1,a=t._x2,o=t._y2;if(t._l01_a>J_){var s=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*s-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*s-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>J_){var u=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,l=3*t._l23_a*(t._l23_a+t._l12_a);a=(a*u+t._x1*t._l23_2a-e*t._l12_2a)/l,o=(o*u+t._y1*t._l23_2a-n*t._l12_2a)/l}t._context.bezierCurveTo(r,i,a,o,t._x2,t._y2)}function ww(t,e){this._context=t,this._alpha=e}ww.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this._y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;break;case 2:this._point=3;default:xw(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const kw=function t(e){function n(t){return e?new ww(t,e):new yw(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function Tw(t,e){this._context=t,this._alpha=e}Tw.prototype={areaStart:iw,areaEnd:iw,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this._y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=e;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=e);break;case 2:this._point=3,this._x5=t,this._y5=e;break;default:xw(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const Ew=function t(e){function n(t){return e?new Tw(t,e):new mw(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function Cw(t,e){this._context=t,this._alpha=e}Cw.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,e){if(t=+t,e=+e,this._point){var n=this._x2-t,r=this._y2-e;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(n*n+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:xw(this,t,e)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=e}};const Sw=function t(e){function n(t){return e?new Cw(t,e):new bw(t,0)}return n.alpha=function(e){return t(+e)},n}(.5);function Aw(t){this._context=t}function Mw(t){return new Aw(t)}function Nw(t){return t<0?-1:1}function Dw(t,e,n){var r=t._x1-t._x0,i=e-t._x1,a=(t._y1-t._y0)/(r||i<0&&-0),o=(n-t._y1)/(i||r<0&&-0),s=(a*i+o*r)/(r+i);return(Nw(a)+Nw(o))*Math.min(Math.abs(a),Math.abs(o),.5*Math.abs(s))||0}function Ow(t,e){var n=t._x1-t._x0;return n?(3*(t._y1-t._y0)/n-e)/2:e}function Bw(t,e,n){var r=t._x0,i=t._y0,a=t._x1,o=t._y1,s=(a-r)/3;t._context.bezierCurveTo(r+s,i+s*e,a-s,o-s*n,a,o)}function Lw(t){this._context=t}function Iw(t){this._context=new Rw(t)}function Rw(t){this._context=t}function Fw(t){return new Lw(t)}function Pw(t){return new Iw(t)}function jw(t){this._context=t}function Yw(t){var e,n,r=t.length-1,i=new Array(r),a=new Array(r),o=new Array(r);for(i[0]=0,a[0]=2,o[0]=t[0]+2*t[1],e=1;e=0;--e)i[e]=(o[e]-i[e+1])/a[e];for(a[r-1]=(t[r]+i[r-1])/2,e=0;e1)for(var n,r,i,a=1,o=t[e[0]],s=o.length;a=0;)n[e]=e;return n}function Gw(t,e){return t[e]}function Xw(){var t=$_([]),e=Vw,n=Ww,r=Gw;function i(i){var a,o,s=t.apply(this,arguments),c=i.length,u=s.length,l=new Array(u);for(a=0;a0){for(var n,r,i,a=0,o=t[0].length;a0)for(var n,r,i,a,o,s,c=0,u=t[e[0]].length;c0?(r[0]=a,r[1]=a+=i):i<0?(r[1]=o,r[0]=o+=i):(r[0]=0,r[1]=i)}function Kw(t,e){if((n=t.length)>0){for(var n,r=0,i=t[e[0]],a=i.length;r0&&(r=(n=t[e[0]]).length)>0){for(var n,r,i,a=0,o=1;oa&&(a=e,r=n);return r}function nk(t){var e=t.map(rk);return Vw(t).sort((function(t,n){return e[t]-e[n]}))}function rk(t){for(var e,n=0,r=-1,i=t.length;++r=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,e){switch(t=+t,e=+e,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,e):this._context.moveTo(t,e);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,e),this._context.lineTo(t,e);else{var n=this._x*(1-this._t)+t*this._t;this._context.lineTo(n,this._y),this._context.lineTo(n,e)}}this._x=t,this._y=e}};var sk="%Y-%m-%dT%H:%M:%S.%LZ",ck=Date.prototype.toISOString?function(t){return t.toISOString()}:gm(sk);const uk=ck;var lk=+new Date("2000-01-01T00:00:00.000Z")?function(t){var e=new Date(t);return isNaN(e)?null:e}:mm(sk);const hk=lk;function fk(t,e,n){var r=new Wn,i=e;return null==e?(r.restart(t,e,n),r):(e=+e,n=null==n?Hn():+n,r.restart((function a(o){o+=i,r.restart(a,i+=e,n),t(o)}),e,n),r)}function dk(t){return function(){return t}}function pk(t){return t[0]}function yk(t){return t[1]}function gk(){this._=null}function mk(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function vk(t,e){var n=e,r=e.R,i=n.U;i?i.L===n?i.L=r:i.R=r:t._=r,r.U=i,n.U=r,n.R=r.L,n.R&&(n.R.U=n),r.L=n}function bk(t,e){var n=e,r=e.L,i=n.U;i?i.L===n?i.L=r:i.R=r:t._=r,r.U=i,n.U=r,n.L=r.R,n.L&&(n.L.U=n),r.R=n}function _k(t){for(;t.L;)t=t.L;return t}gk.prototype={constructor:gk,insert:function(t,e){var n,r,i;if(t){if(e.P=t,e.N=t.N,t.N&&(t.N.P=e),t.N=e,t.R){for(t=t.R;t.L;)t=t.L;t.L=e}else t.R=e;n=t}else this._?(t=_k(this._),e.P=null,e.N=t,t.P=t.L=e,n=t):(e.P=e.N=null,this._=e,n=null);for(e.L=e.R=null,e.U=n,e.C=!0,t=e;n&&n.C;)n===(r=n.U).L?(i=r.R)&&i.C?(n.C=i.C=!1,r.C=!0,t=r):(t===n.R&&(vk(this,n),n=(t=n).U),n.C=!1,r.C=!0,bk(this,r)):(i=r.L)&&i.C?(n.C=i.C=!1,r.C=!0,t=r):(t===n.L&&(bk(this,n),n=(t=n).U),n.C=!1,r.C=!0,vk(this,r)),n=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var e,n,r,i=t.U,a=t.L,o=t.R;if(n=a?o?_k(o):a:o,i?i.L===t?i.L=n:i.R=n:this._=n,a&&o?(r=n.C,n.C=t.C,n.L=a,a.U=n,n!==o?(i=n.U,n.U=t.U,t=n.R,i.L=t,n.R=o,o.U=n):(n.U=i,i=n,t=n.R)):(r=t.C,t=n),t&&(t.U=i),!r)if(t&&t.C)t.C=!1;else{do{if(t===this._)break;if(t===i.L){if((e=i.R).C&&(e.C=!1,i.C=!0,vk(this,i),e=i.R),e.L&&e.L.C||e.R&&e.R.C){e.R&&e.R.C||(e.L.C=!1,e.C=!0,bk(this,e),e=i.R),e.C=i.C,i.C=e.R.C=!1,vk(this,i),t=this._;break}}else if((e=i.L).C&&(e.C=!1,i.C=!0,bk(this,i),e=i.L),e.L&&e.L.C||e.R&&e.R.C){e.L&&e.L.C||(e.R.C=!1,e.C=!0,vk(this,e),e=i.L),e.C=i.C,i.C=e.L.C=!1,bk(this,i),t=this._;break}e.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};const xk=gk;function wk(t,e,n,r){var i=[null,null],a=Wk.push(i)-1;return i.left=t,i.right=e,n&&Tk(i,t,e,n),r&&Tk(i,e,t,r),Hk[t.index].halfedges.push(a),Hk[e.index].halfedges.push(a),i}function kk(t,e,n){var r=[e,n];return r.left=t,r}function Tk(t,e,n,r){t[0]||t[1]?t.left===n?t[1]=r:t[0]=r:(t[0]=r,t.left=e,t.right=n)}function Ek(t,e,n,r,i){var a,o=t[0],s=t[1],c=o[0],u=o[1],l=0,h=1,f=s[0]-c,d=s[1]-u;if(a=e-c,f||!(a>0)){if(a/=f,f<0){if(a0){if(a>h)return;a>l&&(l=a)}if(a=r-c,f||!(a<0)){if(a/=f,f<0){if(a>h)return;a>l&&(l=a)}else if(f>0){if(a0)){if(a/=d,d<0){if(a0){if(a>h)return;a>l&&(l=a)}if(a=i-u,d||!(a<0)){if(a/=d,d<0){if(a>h)return;a>l&&(l=a)}else if(d>0){if(a0||h<1)||(l>0&&(t[0]=[c+l*f,u+l*d]),h<1&&(t[1]=[c+h*f,u+h*d]),!0)}}}}}function Ck(t,e,n,r,i){var a=t[1];if(a)return!0;var o,s,c=t[0],u=t.left,l=t.right,h=u[0],f=u[1],d=l[0],p=l[1],y=(h+d)/2,g=(f+p)/2;if(p===f){if(y=r)return;if(h>d){if(c){if(c[1]>=i)return}else c=[y,n];a=[y,i]}else{if(c){if(c[1]1)if(h>d){if(c){if(c[1]>=i)return}else c=[(n-s)/o,n];a=[(i-s)/o,i]}else{if(c){if(c[1]=r)return}else c=[e,o*e+s];a=[r,o*r+s]}else{if(c){if(c[0]=-Gk)){var d=c*c+u*u,p=l*l+h*h,y=(h*d-u*p)/f,g=(c*p-l*d)/f,m=Dk.pop()||new Ok;m.arc=t,m.site=i,m.x=y+o,m.y=(m.cy=g+s)+Math.sqrt(y*y+g*g),t.circle=m;for(var v=null,b=$k._;b;)if(m.yVk)s=s.L;else{if(!((i=a-Uk(s,o))>Vk)){r>-Vk?(e=s.P,n=s):i>-Vk?(e=s,n=s.N):e=n=s;break}if(!s.R){e=s;break}s=s.R}!function(t){Hk[t.index]={site:t,halfedges:[]}}(t);var c=Fk(t);if(qk.insert(e,c),e||n){if(e===n)return Lk(e),n=Fk(e.site),qk.insert(c,n),c.edge=n.edge=wk(e.site,c.site),Bk(e),void Bk(n);if(n){Lk(e),Lk(n);var u=e.site,l=u[0],h=u[1],f=t[0]-l,d=t[1]-h,p=n.site,y=p[0]-l,g=p[1]-h,m=2*(f*g-d*y),v=f*f+d*d,b=y*y+g*g,_=[(g*v-d*b)/m+l,(f*b-y*v)/m+h];Tk(n.edge,u,p,_),c.edge=wk(u,t,null,_),n.edge=wk(t,p,null,_),Bk(e),Bk(n)}else c.edge=wk(e.site,c.site)}}function zk(t,e){var n=t.site,r=n[0],i=n[1],a=i-e;if(!a)return r;var o=t.P;if(!o)return-1/0;var s=(n=o.site)[0],c=n[1],u=c-e;if(!u)return s;var l=s-r,h=1/a-1/u,f=l/u;return h?(-f+Math.sqrt(f*f-2*h*(l*l/(-2*u)-c+u/2+i-a/2)))/h+r:(r+s)/2}function Uk(t,e){var n=t.N;if(n)return zk(n,e);var r=t.site;return r[1]===e?r[0]:1/0}var qk,Hk,$k,Wk,Vk=1e-6,Gk=1e-12;function Xk(t,e,n){return(t[0]-n[0])*(e[1]-t[1])-(t[0]-e[0])*(n[1]-t[1])}function Zk(t,e){return e[1]-t[1]||e[0]-t[0]}function Qk(t,e){var n,r,i,a=t.sort(Zk).pop();for(Wk=[],Hk=new Array(t.length),qk=new xk,$k=new xk;;)if(i=Nk,a&&(!i||a[1]Vk||Math.abs(i[0][1]-i[1][1])>Vk)||delete Wk[a]}(o,s,c,u),function(t,e,n,r){var i,a,o,s,c,u,l,h,f,d,p,y,g=Hk.length,m=!0;for(i=0;iVk||Math.abs(y-f)>Vk)&&(c.splice(s,0,Wk.push(kk(o,d,Math.abs(p-t)Vk?[t,Math.abs(h-t)Vk?[Math.abs(f-r)Vk?[n,Math.abs(h-n)Vk?[Math.abs(f-e)=s)return null;var c=t-i.site[0],u=e-i.site[1],l=c*c+u*u;do{i=a.cells[r=o],o=null,i.halfedges.forEach((function(n){var r=a.edges[n],s=r.left;if(s!==i.site&&s||(s=r.right)){var c=t-s[0],u=e-s[1],h=c*c+u*u;hr?(r+i)/2:Math.min(0,r)||Math.max(0,i),o>a?(a+o)/2:Math.min(0,a)||Math.max(0,o))}function fT(){var t,e,n=oT,r=sT,i=hT,a=uT,o=lT,s=[0,1/0],c=[[-1/0,-1/0],[1/0,1/0]],u=250,l=Op,h=ft("start","zoom","end"),f=500,d=0;function p(t){t.property("__zoom",cT).on("wheel.zoom",x).on("mousedown.zoom",w).on("dblclick.zoom",k).filter(o).on("touchstart.zoom",T).on("touchmove.zoom",E).on("touchend.zoom touchcancel.zoom",C).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function y(t,e){return(e=Math.max(s[0],Math.min(s[1],e)))===t.k?t:new eT(e,t.x,t.y)}function g(t,e,n){var r=e[0]-n[0]*t.k,i=e[1]-n[1]*t.k;return r===t.x&&i===t.y?t:new eT(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function v(t,e,n){t.on("start.zoom",(function(){b(this,arguments).start()})).on("interrupt.zoom end.zoom",(function(){b(this,arguments).end()})).tween("zoom",(function(){var t=this,i=arguments,a=b(t,i),o=r.apply(t,i),s=null==n?m(o):"function"==typeof n?n.apply(t,i):n,c=Math.max(o[1][0]-o[0][0],o[1][1]-o[0][1]),u=t.__zoom,h="function"==typeof e?e.apply(t,i):e,f=l(u.invert(s).concat(c/u.k),h.invert(s).concat(c/h.k));return function(t){if(1===t)t=h;else{var e=f(t),n=c/e[2];t=new eT(n,s[0]-e[0]*n,s[1]-e[1]*n)}a.zoom(null,t)}}))}function b(t,e,n){return!n&&t.__zooming||new _(t,e)}function _(t,e){this.that=t,this.args=e,this.active=0,this.extent=r.apply(t,e),this.taps=0}function x(){if(n.apply(this,arguments)){var t=b(this,arguments),e=this.__zoom,r=Math.max(s[0],Math.min(s[1],e.k*Math.pow(2,a.apply(this,arguments)))),o=Bn(this);if(t.wheel)t.mouse[0][0]===o[0]&&t.mouse[0][1]===o[1]||(t.mouse[1]=e.invert(t.mouse[0]=o)),clearTimeout(t.wheel);else{if(e.k===r)return;t.mouse=[o,e.invert(o)],ar(this),t.start()}aT(),t.wheel=setTimeout(u,150),t.zoom("mouse",i(g(y(e,r),t.mouse[0],t.mouse[1]),t.extent,c))}function u(){t.wheel=null,t.end()}}function w(){if(!e&&n.apply(this,arguments)){var t=b(this,arguments,!0),r=Te(le.view).on("mousemove.zoom",u,!0).on("mouseup.zoom",l,!0),a=Bn(this),o=le.clientX,s=le.clientY;Se(le.view),iT(),t.mouse=[a,this.__zoom.invert(a)],ar(this),t.start()}function u(){if(aT(),!t.moved){var e=le.clientX-o,n=le.clientY-s;t.moved=e*e+n*n>d}t.zoom("mouse",i(g(t.that.__zoom,t.mouse[0]=Bn(t.that),t.mouse[1]),t.extent,c))}function l(){r.on("mousemove.zoom mouseup.zoom",null),Ae(le.view,t.moved),aT(),t.end()}}function k(){if(n.apply(this,arguments)){var t=this.__zoom,e=Bn(this),a=t.invert(e),o=t.k*(le.shiftKey?.5:2),s=i(g(y(t,o),e,a),r.apply(this,arguments),c);aT(),u>0?Te(this).transition().duration(u).call(v,s,e):Te(this).call(p.transform,s)}}function T(){if(n.apply(this,arguments)){var e,r,i,a,o=le.touches,s=o.length,c=b(this,arguments,le.changedTouches.length===s);for(iT(),r=0;r{t.exports={graphlib:n(574),layout:n(8123),debug:n(7570),util:{time:n(1138).time,notime:n(1138).notime},version:n(8177)}},1207:(t,e,n)=>{"use strict";var r=n(8436),i=n(4079);t.exports={run:function(t){var e="greedy"===t.graph().acyclicer?i(t,function(t){return function(e){return t.edge(e).weight}}(t)):function(t){var e=[],n={},i={};return r.forEach(t.nodes(),(function a(o){r.has(i,o)||(i[o]=!0,n[o]=!0,r.forEach(t.outEdges(o),(function(t){r.has(n,t.w)?e.push(t):a(t.w)})),delete n[o])})),e}(t);r.forEach(e,(function(e){var n=t.edge(e);t.removeEdge(e),n.forwardName=e.name,n.reversed=!0,t.setEdge(e.w,e.v,n,r.uniqueId("rev"))}))},undo:function(t){r.forEach(t.edges(),(function(e){var n=t.edge(e);if(n.reversed){t.removeEdge(e);var r=n.forwardName;delete n.reversed,delete n.forwardName,t.setEdge(e.w,e.v,n,r)}}))}}},1133:(t,e,n)=>{var r=n(8436),i=n(1138);function a(t,e,n,r,a,o){var s={width:0,height:0,rank:o,borderType:e},c=a[e][o-1],u=i.addDummyNode(t,"border",s,n);a[e][o]=u,t.setParent(u,r),c&&t.setEdge(c,u,{weight:1})}t.exports=function(t){r.forEach(t.children(),(function e(n){var i=t.children(n),o=t.node(n);if(i.length&&r.forEach(i,e),r.has(o,"minRank")){o.borderLeft=[],o.borderRight=[];for(var s=o.minRank,c=o.maxRank+1;s{"use strict";var r=n(8436);function i(t){r.forEach(t.nodes(),(function(e){a(t.node(e))})),r.forEach(t.edges(),(function(e){a(t.edge(e))}))}function a(t){var e=t.width;t.width=t.height,t.height=e}function o(t){t.y=-t.y}function s(t){var e=t.x;t.x=t.y,t.y=e}t.exports={adjust:function(t){var e=t.graph().rankdir.toLowerCase();"lr"!==e&&"rl"!==e||i(t)},undo:function(t){var e=t.graph().rankdir.toLowerCase();"bt"!==e&&"rl"!==e||function(t){r.forEach(t.nodes(),(function(e){o(t.node(e))})),r.forEach(t.edges(),(function(e){var n=t.edge(e);r.forEach(n.points,o),r.has(n,"y")&&o(n)}))}(t),"lr"!==e&&"rl"!==e||(function(t){r.forEach(t.nodes(),(function(e){s(t.node(e))})),r.forEach(t.edges(),(function(e){var n=t.edge(e);r.forEach(n.points,s),r.has(n,"x")&&s(n)}))}(t),i(t))}}},7822:t=>{function e(){var t={};t._next=t._prev=t,this._sentinel=t}function n(t){t._prev._next=t._next,t._next._prev=t._prev,delete t._next,delete t._prev}function r(t,e){if("_next"!==t&&"_prev"!==t)return e}t.exports=e,e.prototype.dequeue=function(){var t=this._sentinel,e=t._prev;if(e!==t)return n(e),e},e.prototype.enqueue=function(t){var e=this._sentinel;t._prev&&t._next&&n(t),t._next=e._next,e._next._prev=t,e._next=t,t._prev=e},e.prototype.toString=function(){for(var t=[],e=this._sentinel,n=e._prev;n!==e;)t.push(JSON.stringify(n,r)),n=n._prev;return"["+t.join(", ")+"]"}},7570:(t,e,n)=>{var r=n(8436),i=n(1138),a=n(574).Graph;t.exports={debugOrdering:function(t){var e=i.buildLayerMatrix(t),n=new a({compound:!0,multigraph:!0}).setGraph({});return r.forEach(t.nodes(),(function(e){n.setNode(e,{label:e}),n.setParent(e,"layer"+t.node(e).rank)})),r.forEach(t.edges(),(function(t){n.setEdge(t.v,t.w,{},t.name)})),r.forEach(e,(function(t,e){var i="layer"+e;n.setNode(i,{rank:"same"}),r.reduce(t,(function(t,e){return n.setEdge(t,e,{style:"invis"}),e}))})),n}}},574:(t,e,n)=>{var r;try{r=n(8282)}catch(t){}r||(r=window.graphlib),t.exports=r},4079:(t,e,n)=>{var r=n(8436),i=n(574).Graph,a=n(7822);t.exports=function(t,e){if(t.nodeCount()<=1)return[];var n=function(t,e){var n=new i,o=0,s=0;r.forEach(t.nodes(),(function(t){n.setNode(t,{v:t,in:0,out:0})})),r.forEach(t.edges(),(function(t){var r=n.edge(t.v,t.w)||0,i=e(t),a=r+i;n.setEdge(t.v,t.w,a),s=Math.max(s,n.node(t.v).out+=i),o=Math.max(o,n.node(t.w).in+=i)}));var u=r.range(s+o+3).map((function(){return new a})),l=o+1;return r.forEach(n.nodes(),(function(t){c(u,l,n.node(t))})),{graph:n,buckets:u,zeroIdx:l}}(t,e||o),u=function(t,e,n){for(var r,i=[],a=e[e.length-1],o=e[0];t.nodeCount();){for(;r=o.dequeue();)s(t,e,n,r);for(;r=a.dequeue();)s(t,e,n,r);if(t.nodeCount())for(var c=e.length-2;c>0;--c)if(r=e[c].dequeue()){i=i.concat(s(t,e,n,r,!0));break}}return i}(n.graph,n.buckets,n.zeroIdx);return r.flatten(r.map(u,(function(e){return t.outEdges(e.v,e.w)})),!0)};var o=r.constant(1);function s(t,e,n,i,a){var o=a?[]:void 0;return r.forEach(t.inEdges(i.v),(function(r){var i=t.edge(r),s=t.node(r.v);a&&o.push({v:r.v,w:r.w}),s.out-=i,c(e,n,s)})),r.forEach(t.outEdges(i.v),(function(r){var i=t.edge(r),a=r.w,o=t.node(a);o.in-=i,c(e,n,o)})),t.removeNode(i.v),o}function c(t,e,n){n.out?n.in?t[n.out-n.in+e].enqueue(n):t[t.length-1].enqueue(n):t[0].enqueue(n)}},8123:(t,e,n)=>{"use strict";var r=n(8436),i=n(1207),a=n(5995),o=n(8093),s=n(1138).normalizeRanks,c=n(4219),u=n(1138).removeEmptyRanks,l=n(2981),h=n(1133),f=n(3258),d=n(3408),p=n(7873),y=n(1138),g=n(574).Graph;t.exports=function(t,e){var n=e&&e.debugTiming?y.time:y.notime;n("layout",(function(){var e=n(" buildLayoutGraph",(function(){return function(t){var e=new g({multigraph:!0,compound:!0}),n=C(t.graph());return e.setGraph(r.merge({},v,E(n,m),r.pick(n,b))),r.forEach(t.nodes(),(function(n){var i=C(t.node(n));e.setNode(n,r.defaults(E(i,_),x)),e.setParent(n,t.parent(n))})),r.forEach(t.edges(),(function(n){var i=C(t.edge(n));e.setEdge(n,r.merge({},k,E(i,w),r.pick(i,T)))})),e}(t)}));n(" runLayout",(function(){!function(t,e){e(" makeSpaceForEdgeLabels",(function(){!function(t){var e=t.graph();e.ranksep/=2,r.forEach(t.edges(),(function(n){var r=t.edge(n);r.minlen*=2,"c"!==r.labelpos.toLowerCase()&&("TB"===e.rankdir||"BT"===e.rankdir?r.width+=r.labeloffset:r.height+=r.labeloffset)}))}(t)})),e(" removeSelfEdges",(function(){!function(t){r.forEach(t.edges(),(function(e){if(e.v===e.w){var n=t.node(e.v);n.selfEdges||(n.selfEdges=[]),n.selfEdges.push({e,label:t.edge(e)}),t.removeEdge(e)}}))}(t)})),e(" acyclic",(function(){i.run(t)})),e(" nestingGraph.run",(function(){l.run(t)})),e(" rank",(function(){o(y.asNonCompoundGraph(t))})),e(" injectEdgeLabelProxies",(function(){!function(t){r.forEach(t.edges(),(function(e){var n=t.edge(e);if(n.width&&n.height){var r=t.node(e.v),i={rank:(t.node(e.w).rank-r.rank)/2+r.rank,e};y.addDummyNode(t,"edge-proxy",i,"_ep")}}))}(t)})),e(" removeEmptyRanks",(function(){u(t)})),e(" nestingGraph.cleanup",(function(){l.cleanup(t)})),e(" normalizeRanks",(function(){s(t)})),e(" assignRankMinMax",(function(){!function(t){var e=0;r.forEach(t.nodes(),(function(n){var i=t.node(n);i.borderTop&&(i.minRank=t.node(i.borderTop).rank,i.maxRank=t.node(i.borderBottom).rank,e=r.max(e,i.maxRank))})),t.graph().maxRank=e}(t)})),e(" removeEdgeLabelProxies",(function(){!function(t){r.forEach(t.nodes(),(function(e){var n=t.node(e);"edge-proxy"===n.dummy&&(t.edge(n.e).labelRank=n.rank,t.removeNode(e))}))}(t)})),e(" normalize.run",(function(){a.run(t)})),e(" parentDummyChains",(function(){c(t)})),e(" addBorderSegments",(function(){h(t)})),e(" order",(function(){d(t)})),e(" insertSelfEdges",(function(){!function(t){var e=y.buildLayerMatrix(t);r.forEach(e,(function(e){var n=0;r.forEach(e,(function(e,i){var a=t.node(e);a.order=i+n,r.forEach(a.selfEdges,(function(e){y.addDummyNode(t,"selfedge",{width:e.label.width,height:e.label.height,rank:a.rank,order:i+ ++n,e:e.e,label:e.label},"_se")})),delete a.selfEdges}))}))}(t)})),e(" adjustCoordinateSystem",(function(){f.adjust(t)})),e(" position",(function(){p(t)})),e(" positionSelfEdges",(function(){!function(t){r.forEach(t.nodes(),(function(e){var n=t.node(e);if("selfedge"===n.dummy){var r=t.node(n.e.v),i=r.x+r.width/2,a=r.y,o=n.x-i,s=r.height/2;t.setEdge(n.e,n.label),t.removeNode(e),n.label.points=[{x:i+2*o/3,y:a-s},{x:i+5*o/6,y:a-s},{x:i+o,y:a},{x:i+5*o/6,y:a+s},{x:i+2*o/3,y:a+s}],n.label.x=n.x,n.label.y=n.y}}))}(t)})),e(" removeBorderNodes",(function(){!function(t){r.forEach(t.nodes(),(function(e){if(t.children(e).length){var n=t.node(e),i=t.node(n.borderTop),a=t.node(n.borderBottom),o=t.node(r.last(n.borderLeft)),s=t.node(r.last(n.borderRight));n.width=Math.abs(s.x-o.x),n.height=Math.abs(a.y-i.y),n.x=o.x+n.width/2,n.y=i.y+n.height/2}})),r.forEach(t.nodes(),(function(e){"border"===t.node(e).dummy&&t.removeNode(e)}))}(t)})),e(" normalize.undo",(function(){a.undo(t)})),e(" fixupEdgeLabelCoords",(function(){!function(t){r.forEach(t.edges(),(function(e){var n=t.edge(e);if(r.has(n,"x"))switch("l"!==n.labelpos&&"r"!==n.labelpos||(n.width-=n.labeloffset),n.labelpos){case"l":n.x-=n.width/2+n.labeloffset;break;case"r":n.x+=n.width/2+n.labeloffset}}))}(t)})),e(" undoCoordinateSystem",(function(){f.undo(t)})),e(" translateGraph",(function(){!function(t){var e=Number.POSITIVE_INFINITY,n=0,i=Number.POSITIVE_INFINITY,a=0,o=t.graph(),s=o.marginx||0,c=o.marginy||0;function u(t){var r=t.x,o=t.y,s=t.width,c=t.height;e=Math.min(e,r-s/2),n=Math.max(n,r+s/2),i=Math.min(i,o-c/2),a=Math.max(a,o+c/2)}r.forEach(t.nodes(),(function(e){u(t.node(e))})),r.forEach(t.edges(),(function(e){var n=t.edge(e);r.has(n,"x")&&u(n)})),e-=s,i-=c,r.forEach(t.nodes(),(function(n){var r=t.node(n);r.x-=e,r.y-=i})),r.forEach(t.edges(),(function(n){var a=t.edge(n);r.forEach(a.points,(function(t){t.x-=e,t.y-=i})),r.has(a,"x")&&(a.x-=e),r.has(a,"y")&&(a.y-=i)})),o.width=n-e+s,o.height=a-i+c}(t)})),e(" assignNodeIntersects",(function(){!function(t){r.forEach(t.edges(),(function(e){var n,r,i=t.edge(e),a=t.node(e.v),o=t.node(e.w);i.points?(n=i.points[0],r=i.points[i.points.length-1]):(i.points=[],n=o,r=a),i.points.unshift(y.intersectRect(a,n)),i.points.push(y.intersectRect(o,r))}))}(t)})),e(" reversePoints",(function(){!function(t){r.forEach(t.edges(),(function(e){var n=t.edge(e);n.reversed&&n.points.reverse()}))}(t)})),e(" acyclic.undo",(function(){i.undo(t)}))}(e,n)})),n(" updateInputGraph",(function(){!function(t,e){r.forEach(t.nodes(),(function(n){var r=t.node(n),i=e.node(n);r&&(r.x=i.x,r.y=i.y,e.children(n).length&&(r.width=i.width,r.height=i.height))})),r.forEach(t.edges(),(function(n){var i=t.edge(n),a=e.edge(n);i.points=a.points,r.has(a,"x")&&(i.x=a.x,i.y=a.y)})),t.graph().width=e.graph().width,t.graph().height=e.graph().height}(t,e)}))}))};var m=["nodesep","edgesep","ranksep","marginx","marginy"],v={ranksep:50,edgesep:20,nodesep:50,rankdir:"tb"},b=["acyclicer","ranker","rankdir","align"],_=["width","height"],x={width:0,height:0},w=["minlen","weight","width","height","labeloffset"],k={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"},T=["labelpos"];function E(t,e){return r.mapValues(r.pick(t,e),Number)}function C(t){var e={};return r.forEach(t,(function(t,n){e[n.toLowerCase()]=t})),e}},8436:(t,e,n)=>{var r;try{r={cloneDeep:n(361),constant:n(5703),defaults:n(1747),each:n(6073),filter:n(3105),find:n(3311),flatten:n(5564),forEach:n(4486),forIn:n(2620),has:n(8721),isUndefined:n(2353),last:n(928),map:n(5161),mapValues:n(6604),max:n(6162),merge:n(3857),min:n(3632),minBy:n(2762),now:n(7771),pick:n(9722),range:n(6026),reduce:n(4061),sortBy:n(9734),uniqueId:n(3955),values:n(2628),zipObject:n(7287)}}catch(t){}r||(r=window._),t.exports=r},2981:(t,e,n)=>{var r=n(8436),i=n(1138);function a(t,e,n,o,s,c,u){var l=t.children(u);if(l.length){var h=i.addBorderNode(t,"_bt"),f=i.addBorderNode(t,"_bb"),d=t.node(u);t.setParent(h,u),d.borderTop=h,t.setParent(f,u),d.borderBottom=f,r.forEach(l,(function(r){a(t,e,n,o,s,c,r);var i=t.node(r),l=i.borderTop?i.borderTop:r,d=i.borderBottom?i.borderBottom:r,p=i.borderTop?o:2*o,y=l!==d?1:s-c[u]+1;t.setEdge(h,l,{weight:p,minlen:y,nestingEdge:!0}),t.setEdge(d,f,{weight:p,minlen:y,nestingEdge:!0})})),t.parent(u)||t.setEdge(e,h,{weight:0,minlen:s+c[u]})}else u!==e&&t.setEdge(e,u,{weight:0,minlen:n})}t.exports={run:function(t){var e=i.addDummyNode(t,"root",{},"_root"),n=function(t){var e={};function n(i,a){var o=t.children(i);o&&o.length&&r.forEach(o,(function(t){n(t,a+1)})),e[i]=a}return r.forEach(t.children(),(function(t){n(t,1)})),e}(t),o=r.max(r.values(n))-1,s=2*o+1;t.graph().nestingRoot=e,r.forEach(t.edges(),(function(e){t.edge(e).minlen*=s}));var c=function(t){return r.reduce(t.edges(),(function(e,n){return e+t.edge(n).weight}),0)}(t)+1;r.forEach(t.children(),(function(r){a(t,e,s,c,o,n,r)})),t.graph().nodeRankFactor=s},cleanup:function(t){var e=t.graph();t.removeNode(e.nestingRoot),delete e.nestingRoot,r.forEach(t.edges(),(function(e){t.edge(e).nestingEdge&&t.removeEdge(e)}))}}},5995:(t,e,n)=>{"use strict";var r=n(8436),i=n(1138);t.exports={run:function(t){t.graph().dummyChains=[],r.forEach(t.edges(),(function(e){!function(t,e){var n,r,a,o=e.v,s=t.node(o).rank,c=e.w,u=t.node(c).rank,l=e.name,h=t.edge(e),f=h.labelRank;if(u!==s+1){for(t.removeEdge(e),a=0,++s;s{var r=n(8436);t.exports=function(t,e,n){var i,a={};r.forEach(n,(function(n){for(var r,o,s=t.parent(n);s;){if((r=t.parent(s))?(o=a[r],a[r]=s):(o=i,i=s),o&&o!==s)return void e.setEdge(o,s);s=r}}))}},5439:(t,e,n)=>{var r=n(8436);t.exports=function(t,e){return r.map(e,(function(e){var n=t.inEdges(e);if(n.length){var i=r.reduce(n,(function(e,n){var r=t.edge(n),i=t.node(n.v);return{sum:e.sum+r.weight*i.order,weight:e.weight+r.weight}}),{sum:0,weight:0});return{v:e,barycenter:i.sum/i.weight,weight:i.weight}}return{v:e}}))}},3128:(t,e,n)=>{var r=n(8436),i=n(574).Graph;t.exports=function(t,e,n){var a=function(t){for(var e;t.hasNode(e=r.uniqueId("_root")););return e}(t),o=new i({compound:!0}).setGraph({root:a}).setDefaultNodeLabel((function(e){return t.node(e)}));return r.forEach(t.nodes(),(function(i){var s=t.node(i),c=t.parent(i);(s.rank===e||s.minRank<=e&&e<=s.maxRank)&&(o.setNode(i),o.setParent(i,c||a),r.forEach(t[n](i),(function(e){var n=e.v===i?e.w:e.v,a=o.edge(n,i),s=r.isUndefined(a)?0:a.weight;o.setEdge(n,i,{weight:t.edge(e).weight+s})})),r.has(s,"minRank")&&o.setNode(i,{borderLeft:s.borderLeft[e],borderRight:s.borderRight[e]}))})),o}},6630:(t,e,n)=>{"use strict";var r=n(8436);function i(t,e,n){for(var i=r.zipObject(n,r.map(n,(function(t,e){return e}))),a=r.flatten(r.map(e,(function(e){return r.sortBy(r.map(t.outEdges(e),(function(e){return{pos:i[e.w],weight:t.edge(e).weight}})),"pos")})),!0),o=1;o0;)e%2&&(n+=c[e+1]),c[e=e-1>>1]+=t.weight;u+=t.weight*n}))),u}t.exports=function(t,e){for(var n=0,r=1;r{"use strict";var r=n(8436),i=n(2588),a=n(6630),o=n(1026),s=n(3128),c=n(5093),u=n(574).Graph,l=n(1138);function h(t,e,n){return r.map(e,(function(e){return s(t,e,n)}))}function f(t,e){var n=new u;r.forEach(t,(function(t){var i=t.graph().root,a=o(t,i,n,e);r.forEach(a.vs,(function(e,n){t.node(e).order=n})),c(t,n,a.vs)}))}function d(t,e){r.forEach(e,(function(e){r.forEach(e,(function(e,n){t.node(e).order=n}))}))}t.exports=function(t){var e=l.maxRank(t),n=h(t,r.range(1,e+1),"inEdges"),o=h(t,r.range(e-1,-1,-1),"outEdges"),s=i(t);d(t,s);for(var c,u=Number.POSITIVE_INFINITY,p=0,y=0;y<4;++p,++y){f(p%2?n:o,p%4>=2),s=l.buildLayerMatrix(t);var g=a(t,s);g{"use strict";var r=n(8436);t.exports=function(t){var e={},n=r.filter(t.nodes(),(function(e){return!t.children(e).length})),i=r.max(r.map(n,(function(e){return t.node(e).rank}))),a=r.map(r.range(i+1),(function(){return[]})),o=r.sortBy(n,(function(e){return t.node(e).rank}));return r.forEach(o,(function n(i){if(!r.has(e,i)){e[i]=!0;var o=t.node(i);a[o.rank].push(i),r.forEach(t.successors(i),n)}})),a}},9567:(t,e,n)=>{"use strict";var r=n(8436);t.exports=function(t,e){var n={};return r.forEach(t,(function(t,e){var i=n[t.v]={indegree:0,in:[],out:[],vs:[t.v],i:e};r.isUndefined(t.barycenter)||(i.barycenter=t.barycenter,i.weight=t.weight)})),r.forEach(e.edges(),(function(t){var e=n[t.v],i=n[t.w];r.isUndefined(e)||r.isUndefined(i)||(i.indegree++,e.out.push(n[t.w]))})),function(t){var e=[];function n(t){return function(e){var n,i,a,o;e.merged||(r.isUndefined(e.barycenter)||r.isUndefined(t.barycenter)||e.barycenter>=t.barycenter)&&(i=e,a=0,o=0,(n=t).weight&&(a+=n.barycenter*n.weight,o+=n.weight),i.weight&&(a+=i.barycenter*i.weight,o+=i.weight),n.vs=i.vs.concat(n.vs),n.barycenter=a/o,n.weight=o,n.i=Math.min(i.i,n.i),i.merged=!0)}}function i(e){return function(n){n.in.push(e),0==--n.indegree&&t.push(n)}}for(;t.length;){var a=t.pop();e.push(a),r.forEach(a.in.reverse(),n(a)),r.forEach(a.out,i(a))}return r.map(r.filter(e,(function(t){return!t.merged})),(function(t){return r.pick(t,["vs","i","barycenter","weight"])}))}(r.filter(n,(function(t){return!t.indegree})))}},1026:(t,e,n)=>{var r=n(8436),i=n(5439),a=n(9567),o=n(7304);t.exports=function t(e,n,s,c){var u=e.children(n),l=e.node(n),h=l?l.borderLeft:void 0,f=l?l.borderRight:void 0,d={};h&&(u=r.filter(u,(function(t){return t!==h&&t!==f})));var p=i(e,u);r.forEach(p,(function(n){if(e.children(n.v).length){var i=t(e,n.v,s,c);d[n.v]=i,r.has(i,"barycenter")&&(a=n,o=i,r.isUndefined(a.barycenter)?(a.barycenter=o.barycenter,a.weight=o.weight):(a.barycenter=(a.barycenter*a.weight+o.barycenter*o.weight)/(a.weight+o.weight),a.weight+=o.weight))}var a,o}));var y=a(p,s);!function(t,e){r.forEach(t,(function(t){t.vs=r.flatten(t.vs.map((function(t){return e[t]?e[t].vs:t})),!0)}))}(y,d);var g=o(y,c);if(h&&(g.vs=r.flatten([h,g.vs,f],!0),e.predecessors(h).length)){var m=e.node(e.predecessors(h)[0]),v=e.node(e.predecessors(f)[0]);r.has(g,"barycenter")||(g.barycenter=0,g.weight=0),g.barycenter=(g.barycenter*g.weight+m.order+v.order)/(g.weight+2),g.weight+=2}return g}},7304:(t,e,n)=>{var r=n(8436),i=n(1138);function a(t,e,n){for(var i;e.length&&(i=r.last(e)).i<=n;)e.pop(),t.push(i.vs),n++;return n}t.exports=function(t,e){var n,o=i.partition(t,(function(t){return r.has(t,"barycenter")})),s=o.lhs,c=r.sortBy(o.rhs,(function(t){return-t.i})),u=[],l=0,h=0,f=0;s.sort((n=!!e,function(t,e){return t.barycentere.barycenter?1:n?e.i-t.i:t.i-e.i})),f=a(u,c,f),r.forEach(s,(function(t){f+=t.vs.length,u.push(t.vs),l+=t.barycenter*t.weight,h+=t.weight,f=a(u,c,f)}));var d={vs:r.flatten(u,!0)};return h&&(d.barycenter=l/h,d.weight=h),d}},4219:(t,e,n)=>{var r=n(8436);t.exports=function(t){var e=function(t){var e={},n=0;return r.forEach(t.children(),(function i(a){var o=n;r.forEach(t.children(a),i),e[a]={low:o,lim:n++}})),e}(t);r.forEach(t.graph().dummyChains,(function(n){for(var r=t.node(n),i=r.edgeObj,a=function(t,e,n,r){var i,a,o=[],s=[],c=Math.min(e[n].low,e[r].low),u=Math.max(e[n].lim,e[r].lim);i=n;do{i=t.parent(i),o.push(i)}while(i&&(e[i].low>c||u>e[i].lim));for(a=i,i=r;(i=t.parent(i))!==a;)s.push(i);return{path:o.concat(s.reverse()),lca:a}}(t,e,i.v,i.w),o=a.path,s=a.lca,c=0,u=o[c],l=!0;n!==i.w;){if(r=t.node(n),l){for(;(u=o[c])!==s&&t.node(u).maxRank{"use strict";var r=n(8436),i=n(574).Graph,a=n(1138);function o(t,e){var n={};return r.reduce(e,(function(e,i){var a=0,o=0,s=e.length,u=r.last(i);return r.forEach(i,(function(e,l){var h=function(t,e){if(t.node(e).dummy)return r.find(t.predecessors(e),(function(e){return t.node(e).dummy}))}(t,e),f=h?t.node(h).order:s;(h||e===u)&&(r.forEach(i.slice(o,l+1),(function(e){r.forEach(t.predecessors(e),(function(r){var i=t.node(r),o=i.order;!(os)&&c(n,e,u)}))}))}return r.reduce(e,(function(e,n){var a,o=-1,s=0;return r.forEach(n,(function(r,c){if("border"===t.node(r).dummy){var u=t.predecessors(r);u.length&&(a=t.node(u[0]).order,i(n,s,c,o,a),s=c,o=a)}i(n,s,n.length,a,e.length)})),n})),n}function c(t,e,n){if(e>n){var r=e;e=n,n=r}var i=t[e];i||(t[e]=i={}),i[n]=!0}function u(t,e,n){if(e>n){var i=e;e=n,n=i}return r.has(t[e],n)}function l(t,e,n,i){var a={},o={},s={};return r.forEach(e,(function(t){r.forEach(t,(function(t,e){a[t]=t,o[t]=t,s[t]=e}))})),r.forEach(e,(function(t){var e=-1;r.forEach(t,(function(t){var c=i(t);if(c.length){c=r.sortBy(c,(function(t){return s[t]}));for(var l=(c.length-1)/2,h=Math.floor(l),f=Math.ceil(l);h<=f;++h){var d=c[h];o[t]===t&&e{"use strict";var r=n(8436),i=n(1138),a=n(3573).positionX;t.exports=function(t){(function(t){var e=i.buildLayerMatrix(t),n=t.graph().ranksep,a=0;r.forEach(e,(function(e){var i=r.max(r.map(e,(function(e){return t.node(e).height})));r.forEach(e,(function(e){t.node(e).y=a+i/2})),a+=i+n}))})(t=i.asNonCompoundGraph(t)),r.forEach(a(t),(function(e,n){t.node(n).x=e}))}},300:(t,e,n)=>{"use strict";var r=n(8436),i=n(574).Graph,a=n(6681).slack;function o(t,e){return r.forEach(t.nodes(),(function n(i){r.forEach(e.nodeEdges(i),(function(r){var o=r.v,s=i===o?r.w:o;t.hasNode(s)||a(e,r)||(t.setNode(s,{}),t.setEdge(i,s,{}),n(s))}))})),t.nodeCount()}function s(t,e){return r.minBy(e.edges(),(function(n){if(t.hasNode(n.v)!==t.hasNode(n.w))return a(e,n)}))}function c(t,e,n){r.forEach(t.nodes(),(function(t){e.node(t).rank+=n}))}t.exports=function(t){var e,n,r=new i({directed:!1}),u=t.nodes()[0],l=t.nodeCount();for(r.setNode(u,{});o(r,t){"use strict";var r=n(6681).longestPath,i=n(300),a=n(2472);t.exports=function(t){switch(t.graph().ranker){case"network-simplex":default:!function(t){a(t)}(t);break;case"tight-tree":!function(t){r(t),i(t)}(t);break;case"longest-path":o(t)}};var o=r},2472:(t,e,n)=>{"use strict";var r=n(8436),i=n(300),a=n(6681).slack,o=n(6681).longestPath,s=n(574).alg.preorder,c=n(574).alg.postorder,u=n(1138).simplify;function l(t){t=u(t),o(t);var e,n=i(t);for(d(n),h(n,t);e=y(n);)m(n,t,e,g(n,t,e))}function h(t,e){var n=c(t,t.nodes());n=n.slice(0,n.length-1),r.forEach(n,(function(n){!function(t,e,n){var r=t.node(n).parent;t.edge(n,r).cutvalue=f(t,e,n)}(t,e,n)}))}function f(t,e,n){var i=t.node(n).parent,a=!0,o=e.edge(n,i),s=0;return o||(a=!1,o=e.edge(i,n)),s=o.weight,r.forEach(e.nodeEdges(n),(function(r){var o,c,u=r.v===n,l=u?r.w:r.v;if(l!==i){var h=u===a,f=e.edge(r).weight;if(s+=h?f:-f,o=n,c=l,t.hasEdge(o,c)){var d=t.edge(n,l).cutvalue;s+=h?-d:d}}})),s}function d(t,e){arguments.length<2&&(e=t.nodes()[0]),p(t,{},1,e)}function p(t,e,n,i,a){var o=n,s=t.node(i);return e[i]=!0,r.forEach(t.neighbors(i),(function(a){r.has(e,a)||(n=p(t,e,n,a,i))})),s.low=o,s.lim=n++,a?s.parent=a:delete s.parent,n}function y(t){return r.find(t.edges(),(function(e){return t.edge(e).cutvalue<0}))}function g(t,e,n){var i=n.v,o=n.w;e.hasEdge(i,o)||(i=n.w,o=n.v);var s=t.node(i),c=t.node(o),u=s,l=!1;s.lim>c.lim&&(u=c,l=!0);var h=r.filter(e.edges(),(function(e){return l===v(0,t.node(e.v),u)&&l!==v(0,t.node(e.w),u)}));return r.minBy(h,(function(t){return a(e,t)}))}function m(t,e,n,i){var a=n.v,o=n.w;t.removeEdge(a,o),t.setEdge(i.v,i.w,{}),d(t),h(t,e),function(t,e){var n=r.find(t.nodes(),(function(t){return!e.node(t).parent})),i=s(t,n);i=i.slice(1),r.forEach(i,(function(n){var r=t.node(n).parent,i=e.edge(n,r),a=!1;i||(i=e.edge(r,n),a=!0),e.node(n).rank=e.node(r).rank+(a?i.minlen:-i.minlen)}))}(t,e)}function v(t,e,n){return n.low<=e.lim&&e.lim<=n.lim}t.exports=l,l.initLowLimValues=d,l.initCutValues=h,l.calcCutValue=f,l.leaveEdge=y,l.enterEdge=g,l.exchangeEdges=m},6681:(t,e,n)=>{"use strict";var r=n(8436);t.exports={longestPath:function(t){var e={};r.forEach(t.sources(),(function n(i){var a=t.node(i);if(r.has(e,i))return a.rank;e[i]=!0;var o=r.min(r.map(t.outEdges(i),(function(e){return n(e.w)-t.edge(e).minlen})));return o!==Number.POSITIVE_INFINITY&&null!=o||(o=0),a.rank=o}))},slack:function(t,e){return t.node(e.w).rank-t.node(e.v).rank-t.edge(e).minlen}}},1138:(t,e,n)=>{"use strict";var r=n(8436),i=n(574).Graph;function a(t,e,n,i){var a;do{a=r.uniqueId(i)}while(t.hasNode(a));return n.dummy=e,t.setNode(a,n),a}function o(t){return r.max(r.map(t.nodes(),(function(e){var n=t.node(e).rank;if(!r.isUndefined(n))return n})))}t.exports={addDummyNode:a,simplify:function(t){var e=(new i).setGraph(t.graph());return r.forEach(t.nodes(),(function(n){e.setNode(n,t.node(n))})),r.forEach(t.edges(),(function(n){var r=e.edge(n.v,n.w)||{weight:0,minlen:1},i=t.edge(n);e.setEdge(n.v,n.w,{weight:r.weight+i.weight,minlen:Math.max(r.minlen,i.minlen)})})),e},asNonCompoundGraph:function(t){var e=new i({multigraph:t.isMultigraph()}).setGraph(t.graph());return r.forEach(t.nodes(),(function(n){t.children(n).length||e.setNode(n,t.node(n))})),r.forEach(t.edges(),(function(n){e.setEdge(n,t.edge(n))})),e},successorWeights:function(t){var e=r.map(t.nodes(),(function(e){var n={};return r.forEach(t.outEdges(e),(function(e){n[e.w]=(n[e.w]||0)+t.edge(e).weight})),n}));return r.zipObject(t.nodes(),e)},predecessorWeights:function(t){var e=r.map(t.nodes(),(function(e){var n={};return r.forEach(t.inEdges(e),(function(e){n[e.v]=(n[e.v]||0)+t.edge(e).weight})),n}));return r.zipObject(t.nodes(),e)},intersectRect:function(t,e){var n,r,i=t.x,a=t.y,o=e.x-i,s=e.y-a,c=t.width/2,u=t.height/2;if(!o&&!s)throw new Error("Not possible to find intersection inside of the rectangle");return Math.abs(s)*c>Math.abs(o)*u?(s<0&&(u=-u),n=u*o/s,r=u):(o<0&&(c=-c),n=c,r=c*s/o),{x:i+n,y:a+r}},buildLayerMatrix:function(t){var e=r.map(r.range(o(t)+1),(function(){return[]}));return r.forEach(t.nodes(),(function(n){var i=t.node(n),a=i.rank;r.isUndefined(a)||(e[a][i.order]=n)})),e},normalizeRanks:function(t){var e=r.min(r.map(t.nodes(),(function(e){return t.node(e).rank})));r.forEach(t.nodes(),(function(n){var i=t.node(n);r.has(i,"rank")&&(i.rank-=e)}))},removeEmptyRanks:function(t){var e=r.min(r.map(t.nodes(),(function(e){return t.node(e).rank}))),n=[];r.forEach(t.nodes(),(function(r){var i=t.node(r).rank-e;n[i]||(n[i]=[]),n[i].push(r)}));var i=0,a=t.graph().nodeRankFactor;r.forEach(n,(function(e,n){r.isUndefined(e)&&n%a!=0?--i:i&&r.forEach(e,(function(e){t.node(e).rank+=i}))}))},addBorderNode:function(t,e,n,r){var i={width:0,height:0};return arguments.length>=4&&(i.rank=n,i.order=r),a(t,"border",i,e)},maxRank:o,partition:function(t,e){var n={lhs:[],rhs:[]};return r.forEach(t,(function(t){e(t)?n.lhs.push(t):n.rhs.push(t)})),n},time:function(t,e){var n=r.now();try{return e()}finally{console.log(t+" time: "+(r.now()-n)+"ms")}},notime:function(t,e){return e()}}},8177:t=>{t.exports="0.8.5"},7856:function(t){t.exports=function(){"use strict";var t=Object.hasOwnProperty,e=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,i=Object.getOwnPropertyDescriptor,a=Object.freeze,o=Object.seal,s=Object.create,c="undefined"!=typeof Reflect&&Reflect,u=c.apply,l=c.construct;u||(u=function(t,e,n){return t.apply(e,n)}),a||(a=function(t){return t}),o||(o=function(t){return t}),l||(l=function(t,e){return new(Function.prototype.bind.apply(t,[null].concat(function(t){if(Array.isArray(t)){for(var e=0,n=Array(t.length);e1?n-1:0),i=1;i/gm),j=o(/^data-[\-\w.\u00B7-\uFFFF]/),Y=o(/^aria-[\-\w]+$/),z=o(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),U=o(/^(?:\w+script|data):/i),q=o(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),H="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};function $(t){if(Array.isArray(t)){for(var e=0,n=Array(t.length);e0&&void 0!==arguments[0]?arguments[0]:W(),n=function(e){return t(e)};if(n.version="2.3.5",n.removed=[],!e||!e.document||9!==e.document.nodeType)return n.isSupported=!1,n;var r=e.document,i=e.document,o=e.DocumentFragment,s=e.HTMLTemplateElement,c=e.Node,u=e.Element,l=e.NodeFilter,h=e.NamedNodeMap,w=void 0===h?e.NamedNodeMap||e.MozNamedAttrMap:h,G=e.HTMLFormElement,X=e.DOMParser,Z=e.trustedTypes,Q=u.prototype,K=E(Q,"cloneNode"),J=E(Q,"nextSibling"),tt=E(Q,"childNodes"),et=E(Q,"parentNode");if("function"==typeof s){var nt=i.createElement("template");nt.content&&nt.content.ownerDocument&&(i=nt.content.ownerDocument)}var rt=V(Z,r),it=rt?rt.createHTML(""):"",at=i,ot=at.implementation,st=at.createNodeIterator,ct=at.createDocumentFragment,ut=at.getElementsByTagName,lt=r.importNode,ht={};try{ht=T(i).documentMode?i.documentMode:{}}catch(t){}var ft={};n.isSupported="function"==typeof et&&ot&&void 0!==ot.createHTMLDocument&&9!==ht;var dt=F,pt=P,yt=j,gt=Y,mt=U,vt=q,bt=z,_t=null,xt=k({},[].concat($(C),$(S),$(A),$(N),$(O))),wt=null,kt=k({},[].concat($(B),$(L),$(I),$(R))),Tt=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Et=null,Ct=null,St=!0,At=!0,Mt=!1,Nt=!1,Dt=!1,Ot=!1,Bt=!1,Lt=!1,It=!1,Rt=!1,Ft=!0,Pt=!0,jt=!1,Yt={},zt=null,Ut=k({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),qt=null,Ht=k({},["audio","video","img","source","image","track"]),$t=null,Wt=k({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Vt="http://www.w3.org/1998/Math/MathML",Gt="http://www.w3.org/2000/svg",Xt="http://www.w3.org/1999/xhtml",Zt=Xt,Qt=!1,Kt=void 0,Jt=["application/xhtml+xml","text/html"],te="text/html",ee=void 0,ne=null,re=i.createElement("form"),ie=function(t){return t instanceof RegExp||t instanceof Function},ae=function(t){ne&&ne===t||(t&&"object"===(void 0===t?"undefined":H(t))||(t={}),t=T(t),_t="ALLOWED_TAGS"in t?k({},t.ALLOWED_TAGS):xt,wt="ALLOWED_ATTR"in t?k({},t.ALLOWED_ATTR):kt,$t="ADD_URI_SAFE_ATTR"in t?k(T(Wt),t.ADD_URI_SAFE_ATTR):Wt,qt="ADD_DATA_URI_TAGS"in t?k(T(Ht),t.ADD_DATA_URI_TAGS):Ht,zt="FORBID_CONTENTS"in t?k({},t.FORBID_CONTENTS):Ut,Et="FORBID_TAGS"in t?k({},t.FORBID_TAGS):{},Ct="FORBID_ATTR"in t?k({},t.FORBID_ATTR):{},Yt="USE_PROFILES"in t&&t.USE_PROFILES,St=!1!==t.ALLOW_ARIA_ATTR,At=!1!==t.ALLOW_DATA_ATTR,Mt=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Nt=t.SAFE_FOR_TEMPLATES||!1,Dt=t.WHOLE_DOCUMENT||!1,Lt=t.RETURN_DOM||!1,It=t.RETURN_DOM_FRAGMENT||!1,Rt=t.RETURN_TRUSTED_TYPE||!1,Bt=t.FORCE_BODY||!1,Ft=!1!==t.SANITIZE_DOM,Pt=!1!==t.KEEP_CONTENT,jt=t.IN_PLACE||!1,bt=t.ALLOWED_URI_REGEXP||bt,Zt=t.NAMESPACE||Xt,t.CUSTOM_ELEMENT_HANDLING&&ie(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Tt.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&ie(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Tt.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Tt.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Kt=Kt=-1===Jt.indexOf(t.PARSER_MEDIA_TYPE)?te:t.PARSER_MEDIA_TYPE,ee="application/xhtml+xml"===Kt?function(t){return t}:y,Nt&&(At=!1),It&&(Lt=!0),Yt&&(_t=k({},[].concat($(O))),wt=[],!0===Yt.html&&(k(_t,C),k(wt,B)),!0===Yt.svg&&(k(_t,S),k(wt,L),k(wt,R)),!0===Yt.svgFilters&&(k(_t,A),k(wt,L),k(wt,R)),!0===Yt.mathMl&&(k(_t,N),k(wt,I),k(wt,R))),t.ADD_TAGS&&(_t===xt&&(_t=T(_t)),k(_t,t.ADD_TAGS)),t.ADD_ATTR&&(wt===kt&&(wt=T(wt)),k(wt,t.ADD_ATTR)),t.ADD_URI_SAFE_ATTR&&k($t,t.ADD_URI_SAFE_ATTR),t.FORBID_CONTENTS&&(zt===Ut&&(zt=T(zt)),k(zt,t.FORBID_CONTENTS)),Pt&&(_t["#text"]=!0),Dt&&k(_t,["html","head","body"]),_t.table&&(k(_t,["tbody"]),delete Et.tbody),a&&a(t),ne=t)},oe=k({},["mi","mo","mn","ms","mtext"]),se=k({},["foreignobject","desc","title","annotation-xml"]),ce=k({},S);k(ce,A),k(ce,M);var ue=k({},N);k(ue,D);var le=function(t){var e=et(t);e&&e.tagName||(e={namespaceURI:Xt,tagName:"template"});var n=y(t.tagName),r=y(e.tagName);if(t.namespaceURI===Gt)return e.namespaceURI===Xt?"svg"===n:e.namespaceURI===Vt?"svg"===n&&("annotation-xml"===r||oe[r]):Boolean(ce[n]);if(t.namespaceURI===Vt)return e.namespaceURI===Xt?"math"===n:e.namespaceURI===Gt?"math"===n&&se[r]:Boolean(ue[n]);if(t.namespaceURI===Xt){if(e.namespaceURI===Gt&&!se[r])return!1;if(e.namespaceURI===Vt&&!oe[r])return!1;var i=k({},["title","style","font","a","script"]);return!ue[n]&&(i[n]||!ce[n])}return!1},he=function(t){p(n.removed,{element:t});try{t.parentNode.removeChild(t)}catch(e){try{t.outerHTML=it}catch(e){t.remove()}}},fe=function(t,e){try{p(n.removed,{attribute:e.getAttributeNode(t),from:e})}catch(t){p(n.removed,{attribute:null,from:e})}if(e.removeAttribute(t),"is"===t&&!wt[t])if(Lt||It)try{he(e)}catch(t){}else try{e.setAttribute(t,"")}catch(t){}},de=function(t){var e=void 0,n=void 0;if(Bt)t=""+t;else{var r=g(t,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===Kt&&(t=''+t+"");var a=rt?rt.createHTML(t):t;if(Zt===Xt)try{e=(new X).parseFromString(a,Kt)}catch(t){}if(!e||!e.documentElement){e=ot.createDocument(Zt,"template",null);try{e.documentElement.innerHTML=Qt?"":a}catch(t){}}var o=e.body||e.documentElement;return t&&n&&o.insertBefore(i.createTextNode(n),o.childNodes[0]||null),Zt===Xt?ut.call(e,Dt?"html":"body")[0]:Dt?e.documentElement:o},pe=function(t){return st.call(t.ownerDocument||t,t,l.SHOW_ELEMENT|l.SHOW_COMMENT|l.SHOW_TEXT,null,!1)},ye=function(t){return t instanceof G&&("string"!=typeof t.nodeName||"string"!=typeof t.textContent||"function"!=typeof t.removeChild||!(t.attributes instanceof w)||"function"!=typeof t.removeAttribute||"function"!=typeof t.setAttribute||"string"!=typeof t.namespaceURI||"function"!=typeof t.insertBefore)},ge=function(t){return"object"===(void 0===c?"undefined":H(c))?t instanceof c:t&&"object"===(void 0===t?"undefined":H(t))&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},me=function(t,e,r){ft[t]&&f(ft[t],(function(t){t.call(n,e,r,ne)}))},ve=function(t){var e=void 0;if(me("beforeSanitizeElements",t,null),ye(t))return he(t),!0;if(g(t.nodeName,/[\u0080-\uFFFF]/))return he(t),!0;var r=ee(t.nodeName);if(me("uponSanitizeElement",t,{tagName:r,allowedTags:_t}),!ge(t.firstElementChild)&&(!ge(t.content)||!ge(t.content.firstElementChild))&&_(/<[/\w]/g,t.innerHTML)&&_(/<[/\w]/g,t.textContent))return he(t),!0;if("select"===r&&_(/